为什么 SynchronizationContext.Post() 在使用多个等待时只被调用一次?

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

考虑以下示例:

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

我希望每次等待操作结束时都会调用 SynchronizationContext.Post(),但在覆盖 Post() 之后像这样

public class MySynchronizationContext
{
  public override void Post(SendOrPostCallback d, object? state)
  {
      Console.WriteLine("Continuation: " + Thread.CurrentThread.ManagedThreadId);
      base.Post(d, state);
  }
}

Main()

的开头就这样安装了
SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());

在第一个 Run() 完成后,它只打印一次消息。

我假设那是因为 Task.Run() 可能检测到它在线程池线程上被调用并且只是重用当前线程但情况似乎并非如此,因为我的一些测试导致 运行 2运行 3 运行 在不同的线程上。

为什么 awaited 任务的完成仅在第一个 await 之后运行?

最后我自己弄明白了。

问题似乎是我对 await 捕获当前 SynchronizationContext 的错误理解。

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

SynchronizationContext.SetSynchronizationContext 方法在当前线程上安装提供的 SynchronizationContext。为了让后续的 await 捕获并重用相同的 SynchronizationContextSynchronizationContext 的实现必须确保在原始线程上调用继续,或者它安装自己在它用于调用延续的任何其他线程上。

您的实施 (MySynchronizationContext) 不会这样做。它只是将 Post 调用委托给 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,并使用 ConfigureAwait(false).

配置第一个 await