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:
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
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.
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.
No. It schedules the remaining code to run on the correct context.
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.
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).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With