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.