问题描述
我对以下代码的行为有几件事情(但有一件主要的事情)我不了解.
有人可以帮忙解释一下吗?
这实际上是非常简单的代码-只是一个调用异步方法的常规方法.在异步方法中,我使用using块尝试临时更改SynchronizationContext.
在代码的不同点,我探究了当前的SynchronizationContext.
这是我的问题:
- 当执行到达位置"2.1"时,上下文已更改为 上下文#2.好的.然后,因为我们命中了一个"await",所以任务是 返回并执行跳回到位置"1.2".为什么然后 位置1.2,上下文是否不粘在"上下文2上?
也许using语句和异步方法正在发生一些魔术? - 在位置2.2,为什么上下文不是上下文#2?不应该将上下文延续到继续"("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
方法传入连续委托,该 .当它们在ThreadPool(实际上是)上运行时,它们不依赖于当前SynchronizationContext
是null
来继续这些实例,因此无需进行任何操作即可还原当前实例.需要明确的是,由于您没有使用.ConfigureAwait(false)
,您的延续将被发布到所捕获的上下文中,但是,该实现中的Post
方法不会保留/流动相同的实例.
要解决此问题(即,使上下文变粘"),您可以继承SynchronizationContext
,并重载Post
方法以使用已发布的委托调用SynchronizationContext.SetSynchronizationContext(this)
(使用Delegate.Combine(...)
).另外,在大多数情况下,内部对象将SynchronizationContext
实例与null
相同,因此,如果您想玩这些东西,请务必创建一个继承实现.
对于1.2
,这实际上也让我感到惊讶,因为我的理解是这将调用基础状态机(以及AsyncMethodBuilder
中的所有内部信息),但是在保持其SynchronizationContext
的同时将被同步调用.
我认为我们在这里看到的内容已得到解释在本文中,这与在AsyncMethodBuilder
/异步状态机内部捕获并还原ExecutionContext有关,这是保护并保留了调用ExecutionContext
并因此保留了SynchronizationContext
.可以在此处看到此(谢谢) @VMAtm).
There are a couple of things (but 1 main thing) that I don't understand about the behavior of the following code.
Can someone help explain this?
It's actually pretty simple code - just one regular method calling an async method. And in the async method I use a using block to try to temporarily change the SynchronizationContext.
At different points in the code, I probe for the current SynchronizationContext.
Here are my questions:
- When execution reaches position "2.1" the context has changed to Context #2. Okay. Then, because we hit an `await`, a Task is returned and execution jumps back to position "1.2". Why then, at position 1.2, does the context not "stick" at Context #2?
Maybe there's some magic going on here with the using statement and async methods? - At position 2.2, why is the context not Context #2? Shouldn't the context be carried over into the "continuation" (the statements after `await`)?
Code:
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);
}
}
}
Results:
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
Is quite simple to explain, 1.2
not as easy.
The reason 2.2
prints null
is due to when you await
using the default (new SynchronizationContext
) or null
SynchronizationContext, the Post
method will get called passing in the continuation delegate, this is scheduled on the ThreadPool. It makes no effort to restore the current instance, it relies on the current SynchronizationContext
being null
for these continuations when they run on the ThreadPool (which it is). To be clear, because you are not using .ConfigureAwait(false)
your continuation will get posted to the captured context as you are expecting, but the Post
method in this implementation doesn't preserve/flow the same instance.
To fix this (i.e. make your context "sticky"), you could inherit from SynchronizationContext
, and overload the Post
method to call SynchronizationContext.SetSynchronizationContext(this)
with the posted delegate (using Delegate.Combine(...)
). Also, the internals treat SynchronizationContext
instances the same as null
in most places, so if you want to play with this stuff, always create an inheriting implementation.
For 1.2
, this actually surprised me also, as my understanding was that this would call the underlying state machine (along with all the internals from AsyncMethodBuilder
), but it would be called synchronously while maintaining its SynchronizationContext
.
I think what we are seeing here is explained 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 (thanks @VMAtm).
这篇关于如何解释等待/异步同步上下文切换行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!