Consider the following example:
async Task DoWork()
{
await Task.Run(() =>
{
for (int i = 0; i < 25; i++)
{
Console.WriteLine("Task run 1: " + Thread.CurrentThread.ManagedThreadId);
}
});
// The SynchronizationContext.Post() gets called after Run 1 and before Run 2
await Task.Run(() =>
{
for (int i = 0; i < 25; i++)
{
Console.WriteLine("Task run 2: " + Thread.CurrentThread.ManagedThreadId);
}
});
// I expect it to run after Run 2 and before Run 3 as well but it doesn't
await Task.Run(() =>
{
for (int i = 0; i < 25; i++)
{
Console.WriteLine("Task run 3: " + Thread.CurrentThread.ManagedThreadId);
}
});
}
I would expect a call to SynchronizationContext.Post() to be made every time an await operation ends but after overriding the Post() like this
public class MySynchronizationContext
{
public override void Post(SendOrPostCallback d, object? state)
{
Console.WriteLine("Continuation: " + Thread.CurrentThread.ManagedThreadId);
base.Post(d, state);
}
}
Installed like this at the very start of Main()
SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());
It only prints the message once, after the first Run() is completed.
I assumed that's because Task.Run() may detect that it's being called on a threadpool thread and just reuse the current thread but that seems not to be the case because some of my tests resulted in Run 2 and Run 3 running on different threads.
Why does the completion of an awaited Task only runs after the first await?
The
SynchronizationContext.SetSynchronizationContextmethod installs the suppliedSynchronizationContexton the current thread. In order for the sameSynchronizationContextto be captured and reused by subsequentawaits, the implementation of theSynchronizationContextmust ensure that the continuation is invoked on the original thread, or that it installs itself on any other thread that it uses for invoking the continuation.Your implementation (
MySynchronizationContext) doesn't do that. It just delegates thePostcall to thebase.Post, which invokes the continuation on theThreadPool. TheMySynchronizationContextinstance is not installed on any of theThreadPoolthreads, so the secondawaitfinds nothing to capture, and so the second continuation is invoked on whatever thread theTask.Runmethod completed, which is also aThreadPoolthread. So essentially you get the same behavior that you would get by using a properly implementedSynchronizationContext, like Stephen Cleary'sAsyncContext, and configuring the firstawaitwithConfigureAwait(false).