How to Explain Wait / Async Synchronization Context Switching Behavior

There are a few things (but the main one is 1) that I don't understand about the behavior of the following code.

Can someone explain this?

It's actually pretty simple code - just one regular method calling the async method. And in the async method, I am using a using block to try and temporarily change the SynchronizationContext.

At various points in the code, I check the current SynchronizationContext.

Here are my questions:

  • When execution reaches position "2.1", the context has changed to Context # 2. Good. Then, since we hit await, the Task returns and execution returns to position "1.2". Why then position 1.2, doesn't the context mean "stick to" in the context # 2?

    Maybe there is some magic here using the using statement and asynchronous methods?
  • In position 2.2, why is the context not Context # 2? Shouldn't the context be transferred to the "continuation" (statements after the "wait")?

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

      

+3


source to share


1 answer


2.2

Simple enough to explain, 1.2

not so simple.

The reason it 2.2

prints is null

because when you await

are using default ( new SynchronizationContext

) or null

SynchronizationContext, then the Post

method will be called passing into the continuation of the delegate that's scheduled on the ThreadPool . It does not make any effort to restore the current instance, it relies on the current SynchronizationContext

one being null

for these continuations when they are started in the ThreadPool (which it is). To be clear, since you are not using .ConfigureAwait(false)

, your continuation will be posted to the captured context as you expect, but the method Post

in that implementation does not save / carry over the same instance.

To fix this (ie make your context "sticky"), you can inherit from SynchronizationContext

and overload the method Post

to call SynchronizationContext.SetSynchronizationContext(this)

with the hosted delegate (using Delegate.Combine(...)

). Plus, internals handle instances the SynchronizationContext

same way they do null

most places, so if you want to play with this stuff, always create an inheriting implementation.



For 1.2

this it really surprised me as I figured it would call the underlying state machine (along with all internals from AsyncMethodBuilder

), but it would be called synchronously, keeping it alive SynchronizationContext

.

I think this is where we are explaining in this post and this is because the ExecutionContext is captured and restored inside AsyncMethodBuilder

/ async, it protects and saves the call ExecutionContext

and hence SynchronizationContext

. The code for this can be seen here (thanks @VMAtm).

+2


source







All Articles