SynchronizationContext在Task.Run上流动,但不在await上流动

时间:2015-04-01 07:46:00

标签: c# .net async-await synchronizationcontext

在阅读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 ThreadPoolTask​​Scheduler

     

ProcessMessage ThreadPoolTask​​Scheduler

     

PerformServiceCall 3 ThreadPoolTask​​Scheduler

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

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

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

任何人都可以澄清或指出一些文件吗?

1 个答案:

答案 0 :(得分:7)

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

ExecutionContext总是流淌。即使您使用Task.Run,如果SynchronizationContextTask.Run一起流动Task.Run也会在UI线程上执行,因此SynchronizationContext将毫无意义。 await不会流动,而是在到达异步点(即ExecutionContext)并且在其后发布后继续捕获(除非另有明确说明)。

这句话解释了差异:

  

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

     

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

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

当您使用ConfigureAwait(false)(忽略捕获的SynchronizationContext)时,您只能“取消”用户界面HttpClient.GetAsync。第一次发生在PerformServiceCall,然后再次发送到{{1}}。