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
那样“流动”(尽管 SynchronizationContext
是 ExecutionContext
的一部分)。
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
.
上再次发生
阅读 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
那样“流动”(尽管 SynchronizationContext
是 ExecutionContext
的一部分)。
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 aSynchronizationContext
.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 aSynchronizationContext
. 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, withSynchronizationContext.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 thePost
method.
这意味着在你的情况下,当你输出 PerformServiceCall 1
时,当前 SynchronizationContext
确实是 WindowsFormsSynchronizationContext
因为你还没有到达任何异步点,你仍然在 UI 线程(请记住,async
方法中第一个 await
之前的部分在调用线程上同步执行,因此 LogSyncContext("PerformServiceCall 1");
发生在 ConfigureAwait(false)
发生之前从 PerformServiceCall
).
当您使用 ConfigureAwait(false)
时,您只能“下车”UI 的 SynchronizationContext
(忽略捕获的 SynchronizationContext
)。第一次发生在 HttpClient.GetAsync
上,然后在 PerformServiceCall
.