【问题标题】:SynchronizationContext flows on Task.Run but not on awaitSynchronizationContext 在 Task.Run 上流动,但在 await 上不流动
【发布时间】:2023-03-20 15:38:01
【问题描述】:

阅读Stephen Toub's article on SynchronizationContext 后,我对这段 .NET 4.5 代码的输出有疑问:

private void btnDoSomething_Click()
{
    LogSyncContext("btnDoSomething_Click");
    DoItAsync().Wait();
}
private async Task DoItAsync()
{
    LogSyncContext("DoItAsync");
    await PerformServiceCall().ConfigureAwait(false); //to avoid deadlocking
}
private async Task PerformServiceCall()
{
    LogSyncContext("PerformServiceCall 1");
    HttpResponseMessage message = await new HttpClient
    {
        BaseAddress = new Uri("http://my-service")
    }
    .GetAsync("/").ConfigureAwait(false); //to avoid deadlocking
    LogSyncContext("PerformServiceCall 2");
    await ProcessMessage(message);
    LogSyncContext("PerformServiceCall 3");
}

private async Task ProcessMessage(HttpResponseMessage message)
{
    LogSyncContext("ProcessMessage");
    string data = await message.Content.ReadAsStringAsync();
    //do something with data
}

private static void LogSyncContext(string statementId)
{
    Trace.WriteLine(String.Format("{0} {1}", statementId, SynchronizationContext.Current != null ? SynchronizationContext.Current.GetType().Name : TaskScheduler.Current.GetType().Name));
}

输出是:

btnDoSomething_Click WindowsFormsSynchronizationContext

DoItAsync WindowsFormsSynchronizationContext

PerformServiceCall 1 WindowsFormsSynchronizationContext

PerformServiceCall 2 线程池任务调度器

ProcessMessage ThreadPoolTask​​Scheduler

PerformServiceCall 3 线程池任务调度器

但我希望 PerformServiceCall 1 不会出现在 WindowsFormsSynchronizationContext 上,因为文章指出“SynchronizationContext.Current 不会跨等待点“流动”...

使用 Task.Run 和异步 lambda 调用 PerformServiceCall 时,上下文不会被传递,如下所示:

await Task.Run(async () =>
{
    await PerformServiceCall();
}).ConfigureAwait(false);

任何人都可以澄清或指出一些关于此的文档吗?

【问题讨论】:

  • 在任务实际开始等待之前,ConfigureAwait() 调用不会产生任何影响。那还没有发生,您的 LogSyncContext() 调用太早了。在等待之后移动它。
  • 这不是在`DoItAsync().Wait();`上死锁吗?
  • 不,由于 ConfigureAwait 调用,它没有死锁

标签: c# .net async-await synchronizationcontext


【解决方案1】:

Stephen 的文章解释说 SynchronizationContext 不像 ExecutionContext 那样“流动”(尽管 SynchronizationContextExecutionContext 的一部分)。

ExecutionContext 总是流动的。即使你使用Task.Run,所以如果SynchronizationContext 会随之流动,Task.Run 会在 UI 线程上执行,所以Task.Run 将毫无意义。 SynchronizationContext 不会流动,而是在达到异步点(即await)时捕获,然后将其发布到该点(除非另有明确说明)。

这句话解释了不同之处:

现在,我们要做一个非常重要的观察:流动ExecutionContext 在语义上与捕获并发布到SynchronizationContext 非常不同。

当您流动ExecutionContext 时,您正在从一个线程捕获状态,然后恢复该状态,使其在提供的委托执行期间处于环境状态。当您捕获并使用SynchronizationContext 时,情况并非如此。捕获部分是相同的,因为您正在从当前线程中获取数据,但是您随后会以不同的方式使用该状态。使用SynchronizationContext.Post,您只需使用捕获的状态来调用委托,而不是在调用委托期间使该状态成为当前状态。该委托的运行地点、时间和方式完全取决于Post 方法的实现。

这意味着在您的情况下,当您输出 PerformServiceCall 1 当前 SynchronizationContext 确实是 WindowsFormsSynchronizationContext 因为您尚未达到任何异步点并且您仍在 UI 线程中(请记住,该部分在async 方法中的第一个await 在调用线程上同步执行之前,因此LogSyncContext("PerformServiceCall 1"); 发生在ConfigureAwait(false) 发生在从PerformServiceCall 返回的任务上之前。

只有当您使用 ConfigureAwait(false)(忽略捕获的 SynchronizationContext)时,您才会“离开”用户界面的 SynchronizationContext。第一次发生在HttpClient.GetAsync,然后再次发生在PerformServiceCall

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-03
    • 2014-03-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多