Configureawait Pushes the Continuation to a Pool Thread

ConfigureAwait pushes the continuation to a pool thread

Why ConfigureAwait pro-actively pushes the await continuation to a pool thread here?

It doesn't "push it to a thread pool thread" as much as say "don't force myself to come back to the previous SynchronizationContext".

If you don't capture the existing context, then the continuation which handles the code after that await will just run on a thread pool thread instead, since there is no context to marshal back into.

Now, this is subtly different than "push to a thread pool", since there isn't a guarantee that it will run on a thread pool when you do ConfigureAwait(false). If you call:

await FooAsync().ConfigureAwait(false);

It is possible that FooAsync() will execute synchronously, in which case, you will never leave the current context. In that case, ConfigureAwait(false) has no real effect, since the state machine created by the await feature will short circuit and just run directly.

If you want to see this in action, make an async method like so:

static Task FooAsync(bool runSync)
{
if (!runSync)
await Task.Delay(100);
}

If you call this like:

await FooAsync(true).ConfigureAwait(false);

You'll see that you stay on the main thread (provided that was the current context prior to the await), since there is no actual async code executing in the code path. The same call with FooAsync(false).ConfigureAwait(false); will cause it to jump to thread pool thread after execution, however.

ConfigureAwait(false) - Does the continuation always run on different thread?

When asked in the negative form like that the answer is, i think, pretty clear - there is no guarantee that the second half will be executed on a different thread to the first half. As you speculate, the original thread might well be the lucky next-to-be-picked available thread, when the continuation is up for executing.

Also important to note is that it's the context, not necessarily the thread, that is restored. In the case of a Windows message loop (e.g. WinForms UI thread), it is the UI thread running the message loop that picks up and executes the continuation, hence with ConfigureAwait(true), the same thread is guaranteed. With other SynchronizationContexts, though, there might be no particular reason to require or even prefer the original thread, as long as whatever is considered by them to be "context" is restored; e.g. HttpContext.Current [, identity, culture] in ASP.NET.

There is also an at-least theoretical chance that HeavyIo() completes synchronously, in which case there is no context-switching anyway, and the second half will simply continue on the same thread as the first. I can only assume from your choice of naming ("heavy") that you're implying this won't be an option.

ConfigureAwait on a call which is already running on a threadpool thread?

The ConfigureAwait() only affects the continuation, so your call to OnTheBackground() is called from the context of the calling thread - a new thread is NOT created at that point.

After the await for OnTheBackground() completes, the continuation MAY be on a new thread - but if nothing in OnTheBackground() blocked it may still continue on the calling thread. So you have to be careful about doing compute-bound stuff, even in an async method!

This is best illustrated with an example. Consider the following code for a WPF application with a single button called Button with its click event handled by a Button_Click() method:

bool firstTime = true;

private async void Button_Click(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Button_Click() called on thread " + Thread.CurrentThread.ManagedThreadId);

await OnTheBackground().ConfigureAwait(false);

Debug.WriteLine("Button_Click() continued on thread " + Thread.CurrentThread.ManagedThreadId);
}

public async Task OnTheBackground()
{
Debug.WriteLine("OnTheBackground() called on thread " + Thread.CurrentThread.ManagedThreadId);

if (firstTime)
await Task.Delay(0);
else
await Task.Delay(10);

firstTime = false;

Debug.WriteLine("OnTheBackground() continued on thread " + Thread.CurrentThread.ManagedThreadId);
}

When you click the button the first time, it will wind up calling await Task.Delay(0); inside OnTheBackground(). Because this does not need to block, when the await Task.Delay(0) returns, it continues on the same thread that called it.

Then when this returns to Button_Click(), even though the await in there has .ConfigureAwait(false) it will continue on the original UI thread, because nothing that it called has transitioned to a different thread.

Thus, on first press of the button the output in the debug window is something like:

Button_Click() called on thread 1
OnTheBackground() called on thread 1
OnTheBackground() continued on thread 1
Button_Click() continued on thread 1

Everything is on the same thread, as expected.

However, on second press of the button it ends up calling await Task.Delay(10); which will cause the await to return on a new thread, since Task.Delay(10) will block.

So when it gets back to await OnTheBackground().ConfigureAwait(false);, it will NOT continue on the UI thread, but rather continues on a different thread.

Thus on the second press of the button, the debug output is something like:

Button_Click() called on thread 1
OnTheBackground() called on thread 1
OnTheBackground() continued on thread 1
Button_Click() continued on thread 4

In this case, you can see that the code after the await in Button_Click() is now running on a non-UI thread.

Await continuing on worker thread even with no call to ConfigureAwait(false)

It turns out the problem was a result of attempting to use a class library containing WPF forms from a native Win32 application. An ordinary WinForms or WPF application would initialize the SynchronizationContext in the constructor of Control. This would normally occur during the creating of the "parking" form.

For reasons I can't yet explain, no matter how many controls are created, the SynchronizationContext is never initialized. Manually creating it some time before any of the forms are created allows awaited tasks to continue on the correct thread.

if (SynchronizationContext.Current == null)
{
AsyncOperationManager.SynchronizationContext = new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher);
}

Task::ConfigureAwait - a race condition?

Is there a possibility that tWithCapturedCtx will execute so fast that the continuation will be executed on the captured context?

Sort of.

If tWithCapturedCtx is already finished by the time the await is evaluated (which means tWithoutCapturedCtx is also already finished), then there is no continuation at all. The async method just continues executing synchronously (on the same context).

However, if tWithCapturedCtx is not already finished by the time the await is evaluated (which means tWithoutCapturedCtx is also not already finished), then the continuation is scheduled without the context being captured.

There's another, even smaller, race condition between when the await checks if its awaitable is complete and when await schedules the continuation. If the awaitable completes within that window, then the continuation is just run synchronously (again, on the same context).

In conclusion, ConfigureAwait(false) means "I don't care what context the rest of this method runs in"; it does not mean "run the rest of this method on the thread pool". If you want to say "run this other code on a thread pool thread", then use Task.Run.

await task.ConfigureAwait(false) versus await ContextSwitcher.SwitchToThreadPool()

As others have noted, ConfigureAwait(false) is less necessary with modern code (in particular, since ASP.NET Core has gone mainstream). Whether to use it in your library at this point is a judgement call; personally, I still do use it, but my main async library is very low-level.

especially given the fact the code after await Do1Async().ConfigureAwait(false) will continue on exactly the same conditions as the code after await ContextSwitcher.SwitchToThreadPool() ?

The conditions aren't exactly the same - there's a difference if Do1Async completes synchronously.

Why is the 1st option considered a good practice and this one isn't

As explained by Stephen Toub, the "switcher" approach does allow code like this:

try
{
await Do1Async(); // UI thread
await ContextSwitcher.SwitchToThreadPool();
await Do2Async(); // Thread pool thread
}
catch (Exception)
{
... // Unknown thread
}

Specifically, catch and finally blocks can run in an unknown thread context, depending on runtime behavior. Code that can run on multiple threads is harder to maintain. This is the main reason it was cut from the Async CTP (also bearing in mind that with the language at that time, you couldn't await in a catch or finally, so you couldn't switch to the desired context).

IIRC, this is still better than the ContextSwitcher option, but why?

I think it's important to note that these are all different semantics - in particular, they are all saying quite different things:

  • await x.ConfigureAwait(false) says "I don't care what thread I resume on. It can be the same thread or a thread pool thread, whatever." Note: it does not say "switch to a thread pool thread."
  • await ContextSwitcher() says "Switch to this context and continue executing".
  • await Task.Run(...) says "Run this code on a thread pool thread and then resume me."

Out of all of those, I prefer the first and third. I use ConfigureAwait(false) to say "this method doesn't care about the thread it resumes on", and I use Task.Run to say "run this code on a background thread". I don't like the "switcher" approach because I find it makes the code less maintainable.

Is it safe to just remove the synchronization context like that, which AFAIU would affect the whole synchronous scope after await new SynchronizationContextRemover() ?

Yes; the tricky part is that it needs to affect the synchronous scope. This is why there is no Func<Task> or Func<Task<T>> overloads for my SynchronizationContextSwitcher.NoContext method, which does pretty much the same thing. The one major difference between NoContext and SynchronizationContextRemover is that mine forces a scope (a lambda) in which there is no context and the remover is in the form of a "switcher". So again, mine forces the code to say "run this code without a context", whereas the switcher says "at this point in my method, remove the context, and then continue executing". In my opinion, the explicitly-scoped code is more maintainable (again, considering catch/finally blocks), which is why I use that style of API.

How is this SynchronizationContextRemover better than ContextSwitcher, besides perhaps it has one less switch to a pool thread?

SynchronizationContextRemover and NoContext both stay on the same thread; they just temporarily remove the context on that thread. ContextSwitcher does actually switch to a thread pool thread.

Revisiting Task.ConfigureAwait(continueOnCapturedContext: false)

When you're dealing with asynchronous operations, the overhead of a thread switch is way too small to care about (generally speaking). The purpose of ConfigureAwait(false) is not to induce a thread switch (if necessary), but rather to prevent too much code running on a particular special context.

The reasoning behind this pattern was that "it helps to avoid deadlocks".

And stack dives.

But I do think this is a non-problem in the general case. When I encounter code that doesn't properly use ConfigureAwait, I just wrap it in a Task.Run and move on. The overhead of thread switches isn't worth worrying about.

C# async/await chaining with ConfigureAwait(false)

Definitely not. ConfigureAwait just as it's name suggest configures the await. It only affects the await coupled with it.

ConfigureAwait actually returns a different awaitable type, ConfiguredTaskAwaitable instead of Task which in turn returns a different awaiter type ConfiguredTaskAwaiter instead of TaskAwaiter

If you want to disregard the SynchronizationContext for all your awaits you must use ConfigureAwait(false) for each of them.

If you want to limit the use of ConfigureAwait(false) you can use my NoSynchronizationContextScope (see here) at the very top:

async Task CallerA()
{
using (NoSynchronizationContextScope.Enter())
{
await Method1Async();
}
}

ConfigureAwait and mixing asynchronous with synchronous calls

"For this reason, I suspect, that ConfigureAwait(false) should be called on all the inner async methods (A and B in this case). Is this correct, or the runtime knows what to do after it sees the first call to ConfigureAwait(false)?"

Yes. It should. Because, as you said, the previous call may complete synchronously and also because the previous call may be changed in the future and you don't want to rely on it.

About the Task.Run (which is preferred to Task.Factory.StartNew), the issue is whether to use that in the API's implementation and the answer is almost always no.

Your case is different. If you are on the UI thread and you have extensive work (more than 50ms by Stephen Cleary's suggestion) that can be done in the background there's nothing wrong with offloading that work to a ThreadPool thread to keep the UI responsive. And just like ConfigureAwait I wouldn't rely on the previous call to move you to the ThreadPool as it can also complete synchronously.

So, correct:

var a = await _someInstance.DoSomethingLittle_A_Async().ConfigureAwait(false);
var b = await _someInstance.DoSomethingLittle_B_Async().ConfigureAwait(false);
var c = await Task.Run(() => _someInstance.DoSomethingLittle_C()).ConfigureAwait(false);


Related Topics



Leave a reply



Submit