James James - 2 months ago 38
ASP.NET (C#) Question

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.
});

Answer

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.