What is the benefit of using async with the ASP.NET QueueBackgroundWorkItem method?
HostingEnvironment.QueueBackgroundWorkItem(asyn
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.
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.
Let me distinguish 4 code snippets of which your examples are line 1 & line 4.
(I think some of us read your question as about line 1 vs line 3; others as about line 3 vs line 4. I think your actual question of 1 vs 4 raises both questions).
(– where HE.QueueBWI is actually HostingEnvironment.QueueBackgroundWorkItem() –)
HE.QueueBWI(async ct => { var result=await LongRunningMethodAsync(); /* etc */ });
HE.QueueBWI( ct => { var result= LongRunningMethodAsync().ContinueWith(t => {/*etc*/});});
HE.QueueBWI( ct => LongRunningMethodAsync() );
HE.QueueBWI( ct => LongRunningMethod() );
The benefit of the async keyword in line 1 is that it allows you use of the simple await syntax which is easier on the eye than line 2. But if you aren't using the result, then you don't need to await it and can use line 3 which is even more readable.
What about the advantage of line 3 over line 4? Well, they are calling completely different methods. Who knows what the advantage–or disadvantage–might be?
Of course we expect the naming to mean they achieve the same thing really, and that a method marked ...Async() is doing it asynchronously and (we assume) somehow more efficiently.
In that case, the advantage of line 3 over line 4 is that it might use or block fewer resources to get it's job done. But who knows without reading the code?
The one example I've seen where the ...Async() method is explicitly claimed to be probably-more-efficient without looking at code is FileStream.WriteAsync() when the FileStream has been opening with FileStream(..., FileOptions.IsASync). In this case, the description of FileOptions.IsAsync suggests you will get Overlapped I/O superpowers. Stephen Cleary's blog suggests this may go as far as the I/O happening without any further consumption of thread resource.
It's the only example I've seen of where the ...Async() method is explicitly said to do something more efficient. Perhaps there are more.
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.