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 }