Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to explain await/async Synchronization Context switching behavior

Tags:

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:

  1. 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?
  2. 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
like image 247
N73k Avatar asked Apr 20 '17 01:04

N73k


People also ask

How is async await different from synchronous?

The differences between asynchronous and synchronous include: Async is multi-thread, which means operations or programs can run in parallel. Sync is single-thread, so only one operation or program will run at a time. Async is non-blocking, which means it will send multiple requests to a server.

What is context synchronization?

SynchronizationContext basically is a provider of callback delegates' execution. It is responsible for ensuring that the delegates are run in a given execution context after a particular portion of code (encapsulated inside a Task object in . Net TPL) in a program has completed its execution.

Does await cause context switch?

No. It schedules the remaining code to run on the correct context.

How does async and await work?

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method.


1 Answers

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).

like image 126
Stuart Avatar answered Sep 21 '22 10:09

Stuart