如何解释等待/异步同步上下文切换行为

N73*_*73k 5 .net c# using synchronizationcontext async-await

我对以下代码的行为有几件事情(但有一件主要事情)不了解。

有人可以帮忙解释一下吗?

它实际上是非常简单的代码-只是一个常规方法调用异步方法。在异步方法中,我使用using块尝试临时更改SynchronizationContext。

在代码的不同点,我探查了当前的SynchronizationContext。

这是我的问题:

  1. 当执行到达位置“ 2.1”时,上下文已更改为上下文2。好的。然后,因为我们点击了一个“ await”,任务被返回,执行跳回到位置“ 1.2”。为什么然后在位置1.2处上下文不“粘在”上下文2上?

    也许using语句和异步方法正在发生一些魔术?
  2. 在位置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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

结果:

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
Run Code Online (Sandbox Code Playgroud)

Stu*_*art 4

2.2解释起来很简单,1.2但并不那么容易。

2.2打印的原因null是当您await使用默认的 ( new SynchronizationContext) 或nullSynchronizationContext 时,Post将调用该方法并传入延续委托,这是在 ThreadPool 上安排的。它不努力恢复当前实例,当这些延续在 ThreadPool 上运行时,它依赖于当前的状态(事实确实如此)SynchronizationContextnull需要明确的是,因为您没有使用.ConfigureAwait(false)您的延续,所以它会按照您的预期发布到捕获的上下文,但Post此实现中的方法不会保留/流相同的实例。

要解决此问题(即使您的上下文“粘性”),您可以继承SynchronizationContext,并重载要使用发布的委托Post调用的方法(使用)。此外,内部对待实例的方式与大多数地方相同,因此如果您想使用这些东西,请始终创建一个继承实现。SynchronizationContext.SetSynchronizationContext(this)Delegate.Combine(...)SynchronizationContextnull

对于1.2,这实际上也让我感到惊讶,因为我的理解是这将调用底层状态机(以及来自 的所有内部AsyncMethodBuilder),但它将在维护其SynchronizationContext.

我认为我们在这里看到的内容在这篇文章中得到了解释,它与在 / 异步状态机内部捕获和恢复 ExecutionContext 有关AsyncMethodBuilder,这是保护和保留调用ExecutionContext,因此SynchronizationContext可以在此处查看相关代码(感谢@VMAtm)。