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

发布于 2025-01-09 13:22:36 字数 1656 浏览 0 评论 0原文

考虑以下示例:

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

安装this 在 Main() 的最开始处,

SynchronizationContext.SetSynchronizationContext(new MySynchronizationContext());

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

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

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

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

忆沫 2025-01-16 13:22:36

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

您的实现 (MySynchronizationContext) 不会执行此操作。它只是将 Post 调用委托给 base.Post,它调用ThreadPool上的延续。 MySynchronizationContext 实例未安装在任何 ThreadPool 线程上,因此第二个 await 找不到任何可捕获的内容,因此调用第二个延续在 Task.Run 方法完成的任何线程上,它也是一个 ThreadPool 线程。因此,本质上,您获得的行为与使用正确实现的 SynchronizationContext 获得的行为相同,例如 Stephen Cleary 的 AsyncContext,并配置第一个 await ConfigureAwait(假)

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).

挽清梦 2025-01-16 13:22:36

我最终自己弄清楚了。

问题似乎是我对通过 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);
        }
    });
}

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);
        }
    });
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文