Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why use async with QueueBackgroundWorkItem?

What is the benefit of using async with the ASP.NET QueueBackgroundWorkItem method?

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken =>
{
    var result = await LongRunningMethodAsync();
    // etc.
});

My understanding is that async functions are used to prevent long-running tasks from blocking the main thread. However, in this case aren't we executing the task in its own thread anyway? What is the advantage over the non-async version:

HostingEnvironment.QueueBackgroundWorkItem(cancellationToken =>
{
    var result = LongRunningMethod();
    // etc.
}); 
like image 937
James Avatar asked Apr 29 '16 15:04

James


People also ask

What is the benefit of using async?

with async / await , you write less code and your code will be more maintainable than using the previous asynchronous programming methods such as using plain tasks. async / await is the newer replacement to BackgroundWorker , which has been used on windows forms desktop applications.

What is the point of async?

Note: The purpose of async / await is to simplify the syntax necessary to consume promise-based APIs. The behavior of async / await is similar to combining generators and promises. Async functions always return a promise.

When would you use asynchronous actions?

Asynchronous actions are best when your method is I/O, network-bound, or long-running and parallelizable. Another benefit of an asynchronous action is that it can be more easily canceled by the user than a synchronous request.

Should I use async or not?

Asynchronous loops are necessary when there is a large number of iterations involved or when the operations within the loop are complex. But for simple tasks like iterating through a small array, there is no reason to overcomplicate things by using a complex recursive function.


3 Answers

What is the benefit of using async with the ASP.NET QueueBackgroundWorkItem method?

It allows your background work item to call asynchronous APIs.

My understanding is that async functions are used to prevent long-running tasks from blocking the main thread. However, in this case aren't we executing the task in its own thread anyway?

The advantage of async methods are that they free up the calling thread. There is no "main thread" on ASP.NET - just request threads and thread pool threads out of the box. In this case, an asynchronous background work item would free up a thread pool thread, which may increase scalability.

What is the advantage over the non-async version

Or, you could think of it this way: if LongRunningOperationAsync is a naturally asynchronous operation, then LongRunningOperation will block a thread that could otherwise be used for something else.

like image 181
Stephen Cleary Avatar answered Oct 06 '22 00:10

Stephen Cleary


The information about QueueBackgroundWorkItem on marked answer is great but the conclusion is wrong.

Using async with closure will actually take the QueueBackgroundWorkItem( Func workItem) override, and it will wait for the task to finish, and doing it without holding any thread.

So the answer to this is if you want to perform any sort of IO operation in the workItem closure, using async/await.

like image 36
LarryX Avatar answered Oct 05 '22 23:10

LarryX


What is the benefit of using async with the ASP.NET QueueBackgroundWorkItem method?

Short answer

There is no benefit, in fact you shouldn't use async here!

Long answer

TL;DR

There is no benefit, in fact -- in this specific situation I would actually advise against it. From MSDN:

Differs from a normal ThreadPool work item in that ASP.NET can keep track of how many work items registered through this API are currently running, and the ASP.NET runtime will try to delay AppDomain shutdown until these work items have finished executing. This API cannot be called outside of an ASP.NET-managed AppDomain. The provided CancellationToken will be signaled when the application is shutting down.

QueueBackgroundWorkItem takes a Task-returning callback; the work item will be considered finished when the callback returns.

This explanation loosely indicates that it's managed for you.

According to the "remarks" it supposedly takes a Task returning callback, however the signature in the documentation conflicts with that:

public static void QueueBackgroundWorkItem(
    Action<CancellationToken> workItem
)

They exclude the overload from the documentation, which is confusing and misleading -- but I digress. Microsoft's "Reference Source" to the rescue. This is the source code for the two overloads as well as the internal invocation to the scheduler which does all the magic that we're concerned with.

Side Note

If you have just an ambiguous Action that you want to queue, that's fine as you can see they simply use a completed task for you under the covers, but that seems a little counter-intuitive. Ideally you will actually have a Func<CancellationToken, Task>.

public static void QueueBackgroundWorkItem(
    Action<CancellationToken> workItem) {
    if (workItem == null) {
        throw new ArgumentNullException("workItem");
    }

    QueueBackgroundWorkItem(ct => { workItem(ct); return _completedTask; });
}

public static void QueueBackgroundWorkItem(
    Func<CancellationToken, Task> workItem) {
    if (workItem == null) {
        throw new ArgumentNullException("workItem");
    }
    if (_theHostingEnvironment == null) {
        throw new InvalidOperationException(); // can only be called within an ASP.NET AppDomain
    }

    _theHostingEnvironment.QueueBackgroundWorkItemInternal(workItem);
}

private void QueueBackgroundWorkItemInternal(
    Func<CancellationToken, Task> workItem) {
    Debug.Assert(workItem != null);

    BackgroundWorkScheduler scheduler = Volatile.Read(ref _backgroundWorkScheduler);

    // If the scheduler doesn't exist, lazily create it, but only allow one instance to ever be published to the backing field
    if (scheduler == null) {
        BackgroundWorkScheduler newlyCreatedScheduler = new BackgroundWorkScheduler(UnregisterObject, Misc.WriteUnhandledExceptionToEventLog);
        scheduler = Interlocked.CompareExchange(ref _backgroundWorkScheduler, newlyCreatedScheduler, null) ?? newlyCreatedScheduler;
        if (scheduler == newlyCreatedScheduler) {
            RegisterObject(scheduler); // Only call RegisterObject if we just created the "winning" one
        }
    }

    scheduler.ScheduleWorkItem(workItem);
}

Ultimately you end up with scheduler.ScheduleWorkItem(workItem); where the workItem represents the asynchronous operation Func<CancellationToken, Task>. The source for this can be found here.

As you can see SheduleWorkItem still has our asynchronous operation in the workItem variable, and it actually then calls into ThreadPool.UnsafeQueueUserWorkItem. This calls RunWorkItemImpl which uses async and await -- therefore you do not need to at your top level, and you should not as again it's managed for you.

public void ScheduleWorkItem(Func<CancellationToken, Task> workItem) {
    Debug.Assert(workItem != null);

    if (_cancellationTokenHelper.IsCancellationRequested) {
        return; // we're not going to run this work item
    }

    // Unsafe* since we want to get rid of Principal and other constructs specific to the current ExecutionContext
    ThreadPool.UnsafeQueueUserWorkItem(state => {
        lock (this) {
            if (_cancellationTokenHelper.IsCancellationRequested) {
                return; // we're not going to run this work item
            }
            else {
                _numExecutingWorkItems++;
            }
        }

        RunWorkItemImpl((Func<CancellationToken, Task>)state);
    }, workItem);
}

// we can use 'async void' here since we're guaranteed to be off the AspNetSynchronizationContext
private async void RunWorkItemImpl(Func<CancellationToken, Task> workItem) {
    Task returnedTask = null;
    try {
        returnedTask = workItem(_cancellationTokenHelper.Token);
        await returnedTask.ConfigureAwait(continueOnCapturedContext: false);
    }
    catch (Exception ex) {
        // ---- exceptions caused by the returned task being canceled
        if (returnedTask != null && returnedTask.IsCanceled) {
            return;
        }

        // ---- exceptions caused by CancellationToken.ThrowIfCancellationRequested()
        OperationCanceledException operationCanceledException = ex as OperationCanceledException;
        if (operationCanceledException != null && operationCanceledException.CancellationToken == _cancellationTokenHelper.Token) {
            return;
        }

        _logCallback(AppDomain.CurrentDomain, ex); // method shouldn't throw
    }
    finally {
        WorkItemComplete();
    }
}

There is an even more in-depth read on the internals here.

like image 28
David Pine Avatar answered Oct 05 '22 23:10

David Pine