如何解释 await/async 同步上下文切换行为
How to explain await/async Synchronization Context switching behavior
关于以下代码的行为,有几件事(但主要是一件)我不明白。
谁能帮忙解释一下?
它实际上是非常简单的代码 - 只是一个调用异步方法的常规方法。在异步方法中,我使用 using 块尝试临时更改 SynchronizationContext。
在代码的不同位置,我探测了当前的 SynchronizationContext。
这是我的问题:
-
当执行到达位置“2.1”时,上下文已更改为
上下文#2。好的。然后,因为我们点击了一个 `await`,一个 Task 是
返回并且执行跳回到位置“1.2”。那么,为什么在
位置 1.2,上下文在上下文 #2 处是否 "stick"?
也许 using 语句和 async 方法有一些神奇之处?
-
在位置 2.2,为什么上下文不是上下文 #2?上下文不应该被带入 "continuation"(`await` 之后的语句)吗?
代码:
public class Test
{
public void StartHere()
{
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
this.logCurrentSyncContext("1.1"); // Context #1
Task t = f();
this.logCurrentSyncContext("1.2"); // Context #1, why not Context #2?
t.Wait();
this.logCurrentSyncContext("1.3"); // Context #1
}
private async Task f()
{
using (new ThreadPoolSynchronizationContextBlock())
{
this.logCurrentSyncContext("2.1"); // Context #2
await Task.Delay(7000);
this.logCurrentSyncContext("2.2"); // Context is NULL, why not Context #2?
}
this.logCurrentSyncContext("2.3"); // Context #1
}
// Just show the current Sync Context. Pass in some kind of marker so we know where, in the code, the logging is happening
private void logCurrentSyncContext(object marker)
{
var sc = System.Threading.SynchronizationContext.Current;
System.Diagnostics.Debug.WriteLine(marker + " Thread: " + Thread.CurrentThread.ManagedThreadId + " SyncContext: " + (sc == null? "null" : sc.GetHashCode().ToString()));
}
public class ThreadPoolSynchronizationContextBlock : IDisposable
{
private static readonly SynchronizationContext threadpoolSC = new SynchronizationContext();
private readonly SynchronizationContext original;
public ThreadPoolSynchronizationContextBlock()
{
this.original = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(threadpoolSC);
}
public void Dispose()
{
SynchronizationContext.SetSynchronizationContext(this.original);
}
}
}
结果:
1.1 Thread: 9 SyncContext: 37121646 // I call this "Context #1"
2.1 Thread: 9 SyncContext: 2637164 // I call this "Context #2"
1.2 Thread: 9 SyncContext: 37121646
2.2 Thread: 11 SyncContext: null
2.3 Thread: 11 SyncContext: 37121646
1.3 Thread: 9 SyncContext: 37121646
2.2
解释起来很简单,1.2
没那么容易。
2.2
打印 null
的原因是当您 await
使用默认 (new SynchronizationContext
) 或 null
SynchronizationContext 时, Post
方法将在延续委托中被调用,this is scheduled on the ThreadPool。它不努力恢复当前实例,当它们 运行 在线程池(它是)上时,它依赖于当前 SynchronizationContext
为这些延续 null
。需要明确的是,因为您没有使用 .ConfigureAwait(false)
,您的延续将按照您的预期发布到捕获的上下文中,但是此实现中的 Post
方法不会 preserve/flow 相同的实例.
要解决此问题(即使您的上下文 "sticky"),您可以继承 SynchronizationContext
,并重载 Post
方法以使用发布的委托调用 SynchronizationContext.SetSynchronizationContext(this)
(使用 Delegate.Combine(...)
)。此外,内部结构在大多数地方将 SynchronizationContext
实例与 null
相同,因此如果你想玩这个东西,请始终创建一个继承实现。
对于 1.2
,这实际上也让我感到惊讶,因为我的理解是这将调用底层状态机(以及 AsyncMethodBuilder
的所有内部结构),但它会被同步调用同时保持其 SynchronizationContext
.
我认为我们在这里看到的内容得到了解释 in this post, and it's to do with ExecutionContext being captured and restored inside of the AsyncMethodBuilder
/ async state machine, this is protecting and preserving the calling ExecutionContext
and hence SynchronizationContext
. Code for this can been seen here(感谢 @VMAtm)。
关于以下代码的行为,有几件事(但主要是一件)我不明白。
谁能帮忙解释一下?
它实际上是非常简单的代码 - 只是一个调用异步方法的常规方法。在异步方法中,我使用 using 块尝试临时更改 SynchronizationContext。
在代码的不同位置,我探测了当前的 SynchronizationContext。
这是我的问题:
-
当执行到达位置“2.1”时,上下文已更改为
上下文#2。好的。然后,因为我们点击了一个 `await`,一个 Task 是
返回并且执行跳回到位置“1.2”。那么,为什么在
位置 1.2,上下文在上下文 #2 处是否 "stick"?
也许 using 语句和 async 方法有一些神奇之处?
-
在位置 2.2,为什么上下文不是上下文 #2?上下文不应该被带入 "continuation"(`await` 之后的语句)吗?
代码:
public class Test
{
public void StartHere()
{
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
this.logCurrentSyncContext("1.1"); // Context #1
Task t = f();
this.logCurrentSyncContext("1.2"); // Context #1, why not Context #2?
t.Wait();
this.logCurrentSyncContext("1.3"); // Context #1
}
private async Task f()
{
using (new ThreadPoolSynchronizationContextBlock())
{
this.logCurrentSyncContext("2.1"); // Context #2
await Task.Delay(7000);
this.logCurrentSyncContext("2.2"); // Context is NULL, why not Context #2?
}
this.logCurrentSyncContext("2.3"); // Context #1
}
// Just show the current Sync Context. Pass in some kind of marker so we know where, in the code, the logging is happening
private void logCurrentSyncContext(object marker)
{
var sc = System.Threading.SynchronizationContext.Current;
System.Diagnostics.Debug.WriteLine(marker + " Thread: " + Thread.CurrentThread.ManagedThreadId + " SyncContext: " + (sc == null? "null" : sc.GetHashCode().ToString()));
}
public class ThreadPoolSynchronizationContextBlock : IDisposable
{
private static readonly SynchronizationContext threadpoolSC = new SynchronizationContext();
private readonly SynchronizationContext original;
public ThreadPoolSynchronizationContextBlock()
{
this.original = SynchronizationContext.Current;
SynchronizationContext.SetSynchronizationContext(threadpoolSC);
}
public void Dispose()
{
SynchronizationContext.SetSynchronizationContext(this.original);
}
}
}
结果:
1.1 Thread: 9 SyncContext: 37121646 // I call this "Context #1"
2.1 Thread: 9 SyncContext: 2637164 // I call this "Context #2"
1.2 Thread: 9 SyncContext: 37121646
2.2 Thread: 11 SyncContext: null
2.3 Thread: 11 SyncContext: 37121646
1.3 Thread: 9 SyncContext: 37121646
2.2
解释起来很简单,1.2
没那么容易。
2.2
打印 null
的原因是当您 await
使用默认 (new SynchronizationContext
) 或 null
SynchronizationContext 时, Post
方法将在延续委托中被调用,this is scheduled on the ThreadPool。它不努力恢复当前实例,当它们 运行 在线程池(它是)上时,它依赖于当前 SynchronizationContext
为这些延续 null
。需要明确的是,因为您没有使用 .ConfigureAwait(false)
,您的延续将按照您的预期发布到捕获的上下文中,但是此实现中的 Post
方法不会 preserve/flow 相同的实例.
要解决此问题(即使您的上下文 "sticky"),您可以继承 SynchronizationContext
,并重载 Post
方法以使用发布的委托调用 SynchronizationContext.SetSynchronizationContext(this)
(使用 Delegate.Combine(...)
)。此外,内部结构在大多数地方将 SynchronizationContext
实例与 null
相同,因此如果你想玩这个东西,请始终创建一个继承实现。
对于 1.2
,这实际上也让我感到惊讶,因为我的理解是这将调用底层状态机(以及 AsyncMethodBuilder
的所有内部结构),但它会被同步调用同时保持其 SynchronizationContext
.
我认为我们在这里看到的内容得到了解释 in this post, and it's to do with ExecutionContext being captured and restored inside of the AsyncMethodBuilder
/ async state machine, this is protecting and preserving the calling ExecutionContext
and hence SynchronizationContext
. Code for this can been seen here(感谢 @VMAtm)。