ConfigureAwait pushes the continuation to a pool thread

.net, async-await, c#, multithreading, task-parallel-library

Solution

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.

Problem

Here is some WinForms code: ``` async void Form1_Load(object sender, EventArgs e) { // on the UI thread Debug.WriteLine(new { where = "before", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); var tcs = new TaskCompletionSource<bool>(); this.BeginInvoke(new MethodInvoker(() => tcs.SetResult(true))); await tcs.Task.ContinueWith(t => { // still on the UI thread Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); // on a pool thread Debug.WriteLine(new { where = "after", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); } ``` The output: ``` { where = before, ManagedThreadId = 10, IsThreadPoolThread = False } { where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False } { where = after, ManagedThreadId = 11, IsThreadPoolThread = True } ``` Why does ConfigureAwait pro-actively push the `await` continuation to a pool thread here? I use "pushing to a pool thread" here to describe the case when the primary continuation callback (the `action` parameter to `TaskAwaiter.UnsafeOnCompleted` has been invoked on one thread, but the secondary callback (the one passed to `ConfiguredTaskAwaiter.UnsafeOnCompleted`) is queued to a pool thread. The docs say: continueOnCapturedContext ... true to attempt to marshal the continuation back to the original context captured; otherwise, false. I understand there's `WinFormsSynchronizationContext` installed on the current thread. Still, there is no attempt to marshal to be made, the execution point is already there. Thus, it's more like "never continue on the original context captured"... As expected, there's no thread switch if the execution point is already on a pool thread without a synchronization context: ``` await Task.Delay(100).ContinueWith(t => { // on a pool thread Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); ``` ``` { where = before, ManagedThreadId = 10, IsThreadPoolThread = False } { where = ContinueWith, ManagedThreadId = 6, IsThreadPoolThread = True } { where = after, ManagedThreadId = 6, IsThreadPoolThread = True } ``` Updated, one more test to see if any sync. context is not good enough for continuation (rather than the original one). This is indeed the case: ``` class DumbSyncContext: SynchronizationContext { } // ... Debug.WriteLine(new { where = "before", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); var tcs = new TaskCompletionSource<bool>(); var thread = new Thread(() => { Debug.WriteLine(new { where = "new Thread", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread}); SynchronizationContext.SetSynchronizationContext(new DumbSyncContext()); tcs.SetResult(true); Thread.Sleep(1000); }); thread.Start(); await tcs.Task.ContinueWith(t => { Debug.WriteLine(new { where = "ContinueWith", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread}); }, TaskContinuationOptions.ExecuteSynchronously).ConfigureAwait(false); Debug.WriteLine(new { where = "after", Thread.CurrentThread.ManagedThreadId, Thread.CurrentThread.IsThreadPoolThread }); ``` ``` { where = before, ManagedThreadId = 9, IsThreadPoolThread = False } { where = new Thread, ManagedThreadId = 10, IsThreadPoolThread = False } { where = ContinueWith, ManagedThreadId = 10, IsThreadPoolThread = False } { where = after, ManagedThreadId = 6, IsThreadPoolThread = True } ```

Original source

Related problems