'Why does SynchronizationContext.Post() get called only once when using multiple awaits?

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?



Solution 1:[1]

I ended up figuring it out on my own.

The problem seemed to be my invalid understanding of capturing current SynchronizationContext by the await.

async Task DoWork()
{
    // This is still in the main thread so SynchronizationContext.Current
    // returns an instance of MySynchronizationContext which this
    // await captures.
    await Task.Run(() =>
    {
        for (int i = 0; i < 25; i++)
        {
            Console.WriteLine("Task run 1: " + Thread.CurrentThread.ManagedThreadId);
        }
    });
    // Here it uses the captured MySynchronizationContext to call
    // the .Post() method. The message gets printed to the console and
    // continuation gets put on the ThreadPool

    // This await tries to capture current SynchronizationContext but
    // since we're on the ThreadPool's thread, SynchronizationContext.Current
    // returns null and it uses the default implementation
    // instead of MySynchronizationContext. This is why my message from
    // the overriden .Post() doesn't get printed which made me believe
    // that it didn't call .Post() at all. It did, just not my .Post()
    await Task.Run(() =>
    {
        for (int i = 0; i < 25; i++)
        {
            Console.WriteLine("Task run 2: " + Thread.CurrentThread.ManagedThreadId);
        }
    });
    // .Post() gets called on the default SynchronizationContext

    // Again, we're on the ThreadPool's thread,
    // so the default SynchronizationContext gets captured
    await Task.Run(() =>
    {
        for (int i = 0; i < 25; i++)
        {
            Console.WriteLine("Task run 3: " + Thread.CurrentThread.ManagedThreadId);
        }
    });
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1