18

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.
}); 
James
  • 7,343
  • 9
  • 46
  • 82
  • *My understanding is that async functions are used to prevent long-running tasks from blocking the main thread* - Well, almost. Async functions are used to prevent **any** thread from blocking. – shay__ Apr 29 '16 at 21:34
  • I'd suggest changing the accepted to Stephen Cleary's. The current accepted answer, while well thought out, is technically incorrect in saying ''there is no advantage, don't do it." There is certainly an advantage. – Todd Menier Mar 10 '17 at 22:25
  • 2
    Assuming `LongRunningMethodAsync` is truly asynchronous, such as an HTTP call, then in your first example the threadpool thread that is assigned to run the background job is returned to the threadpool, where it is free to do other work, for the duration of that asynchronous operation. To the contrary, `LongRunningMethod` is presumably a synchronous, blocking version of the same operation, and your threadpool thread would be tied up (blocked) for the duration of the operation. – Todd Menier Mar 10 '17 at 22:26

4 Answers4

19

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.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • But..but... this is a spurious distinction isn't it? Yes, LongRunningOperation will block a thread when it runs; but so will LongRunningOperationAsync. There is no magic, 'run without using a thread'. As you say, it's the calling thread that gets freed up. And in *both* OPs examples, that freeing up is done for you by HostingEnvironment.QueueBackgroundWorkItem – Chris F Carroll Jan 24 '17 at 01:41
  • 1
    @ChrisFCarroll: No; *if* `LongRunningOperationAsync` is a naturally asynchronous operation, then it does not block a thread. I have more info on how this works [here](http://blog.stephencleary.com/2013/11/there-is-no-thread.html). – Stephen Cleary Jan 24 '17 at 15:06
  • Try to define "naturally asynchronous"? I hypothesize that when you do, the matching criticism will become: But..but... this is a spurious distinction isn't it? LongRunningOperation will not block a thread when it runs because it is naturally asynchronous. Wrapping it in an extra async/await does not make it super-super-asynchronous. Here's my example (I'm assuming that StreamWriter.WriteAsync will fit your meaning of naturally async?) https://gist.github.com/chrisfcarroll/7503a3cc14379b8159902fab572b5d7b – Chris F Carroll Jan 24 '17 at 16:11
  • @ChrisFCarroll: A simple example of naturally asynchronous is OVERLAPPED on windows. Did you read my article? Wrapping in `async`/`await` has nothing to do with it. In your example, `WriteAsync` is not naturally asynchronous - this is because the file APIs are odd. You would need to first construct a file stream explicitly requesting asynchronous I/O, and then use that in the `StreamWriter`. – Stephen Cleary Jan 24 '17 at 20:54
  • I did read it and it did not disabuse me of the notion that if LongRunningOperation is 'naturally async' then it isn't going to block. But yes, I have only just read the notes on FileStream(... FileOptions.IsAsync) so I can re-write my example usong that and … report back. Thx. – Chris F Carroll Jan 24 '17 at 21:58
  • @ChrisFCarroll: In particular, note the part that points out there there is no thread: "How many threads are processing it? None." There's no thread being blocked at that point. Where is the blocked thread? – Stephen Cleary Jan 25 '17 at 00:28
  • So I updated the gist to use a FileStream(FileOptions.Asynchronous) and I draw the same conclusion. In those cases where your point "It does not block a thread/there is no thread" holds, then it continues to hold without needing to wrap it in async/await. So it still looks to me that wrapping async/await round something is quite redundant? – Chris F Carroll Jan 25 '17 at 17:40
  • @ChrisFCarroll: As I have already said, "Wrapping in async/await has nothing to do with it." I don't know how else to explain it to you. If you still have questions, I encourage you to ask it as a separate question. – Stephen Cleary Jan 25 '17 at 17:50
  • Ah. This conversation might make sense if I guess that you use async to mean, the …Async suffix on LongRunningOperationAsync and the implication that there is something genuinely asynchronous happening inside the method; whereas I use async to mean just the async keyword. I think this ambiguity exists in OP and hence the accepted answer is not answering the same question as you are answering. – Chris F Carroll Jan 26 '17 at 09:59
  • 1
    IMO, this is the one and only correct answer to the question asked. – Todd Menier Mar 10 '17 at 22:29
3

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.

LarryX
  • 591
  • 2
  • 7
  • Whilst you're right about the overload, you're wrong to conclude that there is any performance difference between the two overloads. One is an api for when you have a Task; one is an api for when you just have an Action. They both claim to "Schedule a task which can run in the background." – Chris F Carroll Jan 24 '17 at 01:28
  • 1
    @ChrisFCarroll I disagree. The example is hypothetical, and it is reasonable to assume that `LongRunningMethodAsync` is the asynchronous/non-blocking version of `LongRunningMethod`. And in a high concurrency environment like ASP.NET, it is always more efficient (from a thread conservation/scalability standpoint) to favor the asynchronous version of an operation when the choice exists. This is a good and correct answer. – Todd Menier Mar 10 '17 at 22:45
  • @ToddMenier I mostly disagree :-) Putting it on the background thread already addresses the blocking issue. So then where does the belief that the ...Async version is more efficient come from? The only place I've seen in any docs anywhere where ...Async is claimed to be more efficient is https://msdn.microsoft.com/en-us/library/system.io.filestream.isasync(v=vs.110).aspx. – Chris F Carroll Mar 13 '17 at 17:05
  • 1
    @ChrisFCarroll Putting it on a background thread does just that - puts it on anther thread and blocks that thread, if you're calling a synchronous API. Say the background work consists of making an HTTP call that takes 2 seconds to complete. If 1000 simultaneous requests come in, all queued to background, you're consuming 1000 threadpool threads for 2 seconds. If you had used an async API like HttpClient, guess how many threads are consumed for that same 2 second duration? Zero. – Todd Menier Mar 13 '17 at 18:17
  • "Where does the belief that the ...Async version is more efficient come from?" Microsoft, primarily. But just to be clear, what is your definition of "efficient"? If you mean thread efficiency, i.e. not blocking a thread during I/O-bound work is more efficient than blocking one, then we're on the same page. If that's the case, then are you suggesting that most .NET APIs purporting to behave this way actually don't? In that case we should be outraged that Stephen Toub and @StephenCleary and others have been lying to us this whole time! – Todd Menier Mar 13 '17 at 18:31
  • ' "Where does the belief that the ...Async version is more efficient come from?" Microsoft, primarily' Again, where? The claim generally made by Async methods is that they spawn a Task and do not block your main thread. This is the same promise given by QueueBackgroundWorkItem. But you are suggesting that there is something extra extra special about e.g. HttpClient.GetResponseAsync so that it not only doesn't block the main thread but is also, in some other way, more efficient. What has led you to this belief? – Chris F Carroll Mar 19 '17 at 17:02
  • If you have code to show that HostingEnvironment.QueueBackgroundWorkItem(async tk => await httpClient.GetResponseAsync) uses fewer threads than without the async await ...Async that would be great, but I'm finding it quite hard to measure (especially in an actual running asp.net HostingEnvironment), and the best I've got so far suggests to me it makes no odds. – Chris F Carroll Mar 19 '17 at 17:15
  • 1
    @ChrisFCarroll "The claim generally made by Async methods is that they spawn a Task and do not block your main thread." -- this is where the confusion lies: this statement is false. There are other ways that async can be implemented that do not block, but without spawning a Task and burning a thread (e.g. IO completion ports). As a trivial example consider Thread.Sleep vs Task.Delay. – Mark Sowul Dec 23 '20 at 15:27
2

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.

Community
  • 1
  • 1
David Pine
  • 23,787
  • 10
  • 79
  • 107
  • 2
    This is a bad suggestion. You should definitly use an async method here, so the thread can be released back to the pool when it begins an I/O operational. There are 2 overloads. One that takes and action, and one that does not, so I'm not sure why you're saying there's a contradiction. Also, you say it "uses async and await -- therefore you do not need to at your top level", but that's just wrong. Async/await must be used throughout the entire calling chain. If you don't use async-await and you make an I/O operation, you'll have to block on the result instead of awaiting it. – Triynko Nov 26 '18 at 18:16
  • So not true. As soon as you start working with SQL servers, for example, you still want them to be done async. – Gargoyle Jul 12 '19 at 16:52
  • They are two different overloads - one if your action is async, one if it isn't. It's unclear how this dissection supports the conclusion that you *shouldn't* use the async version if you can. If you don't have a Task (i.e. you use the 'Action' version), then the API has to do all this wrapping. If you already have a Task, the API doesn't need to do the extra work. Not to mention the general scalability benefits of using async – Mark Sowul Dec 23 '20 at 15:31
-1

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.

Chris F Carroll
  • 11,146
  • 3
  • 53
  • 61