Why use async with QueueBackgroundWorkItem?

14,692

Solution 1

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.

Solution 2

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.

Solution 3

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.

Share:
14,692
James
Author by

James

Assistant Headteacher at a UK secondary school Creator of MiniTest, an app for online marking and feedback

Updated on June 16, 2022

Comments

  • James
    James almost 2 years

    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.
    }); 
    
  • Chris F Carroll
    Chris F Carroll over 7 years
    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
    Chris F Carroll over 7 years
    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
  • Stephen Cleary
    Stephen Cleary over 7 years
    @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.
  • Chris F Carroll
    Chris F Carroll over 7 years
    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?) gist.github.com/chrisfcarroll/7503a3cc14379b8159902fab572b5d‌​7b
  • Stephen Cleary
    Stephen Cleary over 7 years
    @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.
  • Chris F Carroll
    Chris F Carroll over 7 years
    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.
  • Stephen Cleary
    Stephen Cleary over 7 years
    @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?
  • Chris F Carroll
    Chris F Carroll over 7 years
    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?
  • Stephen Cleary
    Stephen Cleary over 7 years
    @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.
  • Chris F Carroll
    Chris F Carroll over 7 years
    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.
  • Todd Menier
    Todd Menier about 7 years
    IMO, this is the one and only correct answer to the question asked.
  • Todd Menier
    Todd Menier about 7 years
    @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.
  • Chris F Carroll
    Chris F Carroll about 7 years
    @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 msdn.microsoft.com/en-us/library/….
  • Todd Menier
    Todd Menier about 7 years
    @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
    Todd Menier about 7 years
    "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!
  • Chris F Carroll
    Chris F Carroll about 7 years
    ' "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
    Chris F Carroll about 7 years
    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.
  • Triynko
    Triynko over 5 years
    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.
  • Gargoyle
    Gargoyle almost 5 years
    So not true. As soon as you start working with SQL servers, for example, you still want them to be done async.
  • Mark Sowul
    Mark Sowul over 3 years
    @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
    Mark Sowul over 3 years
    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