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

Time:02-24

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?

CodePudding user response:

The SynchronizationContext.SetSynchronizationContext method installs the supplied SynchronizationContext on the current thread. In order for the same SynchronizationContext to be captured and reused by subsequent awaits, the implementation of the SynchronizationContext must 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 the Post call to the base.Post, which invokes the continuation on the ThreadPool. The MySynchronizationContext instance is not installed on any of the ThreadPool threads, so the second await finds nothing to capture, and so the second continuation is invoked on whatever thread the Task.Run method completed, which is also a ThreadPool thread. So essentially you get the same behavior that you would get by using a properly implemented SynchronizationContext, like Stephen Cleary's AsyncContext, and configuring the first await with ConfigureAwait(false).

CodePudding user response:

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);
        }
    });
}
  • Related