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

SynchronizationContext flows on Task.Run but not on await

阅读 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 ThreadPoolTaskScheduler

ProcessMessage ThreadPoolTaskScheduler

PerformServiceCall 3 ThreadPoolTaskScheduler

但我希望 PerformServiceCall 1 不在 WindowsFormsSynchronizationContext 上,因为文章指出 "SynchronizationContext.Current does not “flow” across await points"...

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

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

任何人都可以对此进行澄清或指出一些文档吗?

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

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

区别在这句话中解释:

Now, we have a very important observation to make: flowing ExecutionContext is semantically very different than capturing and posting to a SynchronizationContext.

When you flow ExecutionContext, you’re capturing the state from one thread and then restoring that state such that it’s ambient during the supplied delegate’s execution. That’s not what happens when you capture and use a SynchronizationContext. The capturing part is the same, in that you’re grabbing data from the current thread, but you then use that state differently. Rather than making that state current during the invocation of the delegate, with SynchronizationContext.Post you’re simply using that captured state to invoke the delegate. Where and when and how that delegate runs is completely up to the implementation of the Post method.

这意味着在你的情况下,当你输出 PerformServiceCall 1 时,当前 SynchronizationContext 确实是 WindowsFormsSynchronizationContext 因为你还没有到达任何异步点,你仍然在 UI 线程(请记住,async 方法中第一个 await 之前的部分在调用线程上同步执行,因此 LogSyncContext("PerformServiceCall 1"); 发生在 ConfigureAwait(false) 发生之前从 PerformServiceCall).

返回的任务

当您使用 ConfigureAwait(false) 时,您只能“下车”UI 的 SynchronizationContext(忽略捕获的 SynchronizationContext)。第一次发生在 HttpClient.GetAsync 上,然后在 PerformServiceCall.

上再次发生