SynchronizationContext is one of those topics that deserves a better understanding if we want to fully know how asynchorony works in .Net. It’s true that most of these concerns handled behind the scene. But we can benefit by understanding what exactly happens when we offload a task to a worker thread or release the thread back to the caller. So In this post I’m going to go into detail about what the
SynchronizationContext is and what it does. I also examine how this is different in various frameworks and what
await keyword does for us behind the scene in relation to SynchronizationContext.
What is SynchronizationContext
Formal MSDN Definition
Here’s an explanation about SynchronizationContext from MSDN.
Provides the basic functionality for propagating a synchronization context in various synchronization models. The purpose of the synchronization model implemented by this class is to allow the internal asynchronous/synchronous operations of the common language runtime to behave properly with different synchronization models. This model also simplifies some of the requirements that managed applications have had to follow in order to work correctly under different synchronization environments.
Hmm, huh? except the first part, that might sounds like a bunch of gobbledygook, but I try to give more context about this definition.
Definition With More Context
What above explanation is trying to say is different frameworks has different way of communicating between threads. There’s a lot of reasons why we need to do that. One of which might be we want to invoke a specific code in correct context (read thread). So the Windows form has
Control.BeginInvoke or WPF has
Dispatcher.BeginInvoke which allows us to run our code in the context of calling thread from another thread. Now what SynchronizationContext does is that it act as an abstraction for all of them, you could say it serves as an abstract class that can be used to represent current location in different frameworks.
SynchronizationContext exposes several virtual methods, but let’s focus on
Post for now. Post accepts a delegate, but the responsibility of determining when and where to run that delegate lies with the implementation of it. The default implementation of
SynchronizationContext.Post just passes it to
QueueUserWorkItem. But frameworks can derive their own context from SynchronizationContext and override the
Post method. For example Windows Forms has
WindowsFormsSynchronizationContext that implements Post and pass the delegate to
Control.BeginInvoke. WPF has
DispatcherSynchronizationContext, it calls to
More Relevant Definition
But the previous explanations is not very helpful. Because some people don’t know why we need
Dispatcher.BeginInvoke in the first place. So I’ll try to explain it from another angle. That is the actual problem that SynchronizationContext solves. Not what it replace or rather abstract in different frameworks.
SynchronizationContext is a representation of the current environment that our code is running in. That is, in an asynchronous program, when we delegate a unit of work to another thread, we capture the current environment and store it in an instance of
SynchronizationContext and place it on Task object. The important thing is we capture the current environment and pass it to another thread. How other threads going to use the passed in context is different based on frameworks and not all frameworks need to do that. We’ll see that Asp.Net Core doesn’t have
SynchronizationContext. Here’s another good explanation from an Stackoverflow question.
Simply put, SynchronizationContext represents a location “where” code might be executed. Delegates that are passed to its Send or Post method will then be invoked in that location. (Post is the non-blocking / asynchronous version of Send.)
When and Why Capturing Current SynchronicationContext is Needed
Why it’s Needed
But the important question is, why do we need to capture the current location or environment before moving to another thread? The answer is, there might be a lot of reasons, and there might be none. The general reason is that we need a way to communicate between threads. For example maybe we need to run a certain code in certain location but in a correct thread. Imagine we delegate a piece of code to run on another thread and we await it. Now whatever comes after the await keyword is passed to that thread to be executed as part of our continuation block. Take a look at this two piece of code.
What happens in the above code is that some piece of code after await is executed as a continuation block. Now imagine this continuation run on another thread. Now some operations can be done on other threads, and some can’t. The things is, for those operations that is not allowed, we need to execute the continuation part in the context of the calling thread. That why we need a representation of the location that caller issued the call to another thread. I’ll give you an example of why we need to do that in WPF apps and what happens if we don’t.
Why Capturing the Current Context is Needed In WPF Apps
One example for when it’s necessary is in WPF apps. Imagine we delegate the some kind of operation to another thread, and we want to use the result for setting a text box
Text property. But the problem is, in this framework, only the thread that creates the UI element has the right to change its property. If we try to change a UI element from another thread, we get this error.
This is just one problem that can occur when we want to do something on another thread. Another problem might be our Security Context that allow one operation in one thread and doesn’t allow it in another. So what we can do in this kind of situation is that we capture the current running environment in an
SynchronizationContext instance and pass it to another thread.
In the above code, we capture the current context and do some work on thread pool. When we finished, we need to continue executing the rest of our program. But we want to do it in the correct context. That is the working environment of the UI thread and not the thread pool thread. So we pass what’s needed to be done to Post method of our
SynchronizationContext and that code runs on UI thread. By doing that we solve the problem of object’s thread ownership.
Not All Frameworks Need SynchronizationContext
But some frameworks depending on their internal implementation don’t need SynchronizationContext. Asp.Net Core is one such framework. Read more about it here. In short in such frameworks there might be no need for ConfigureAwait(false). That’s because the running thread doesn’t have a
SynchronizationContext.Current and it’s null. If the current context is null, then it’s going to run on thread pool thread anyway. So we can do need to instruct our program to not capture the context, because there’s none.
Every Thread Has A SynchronizationContext
Another important thing is that every Thread has its own SynchronizationContext. That means if we delegate work from one thread pool to another thread, we can get the snapshot of the current running environment and pass it to another thread. How the framework handle these is not as important as understanding what SynchronizationContext is and what is does for us.
What async/await Does With SynchronizationContext Under the Hood
It’s beneficial to know what happens when we await something and what this syntactic sugar turns into. This roughly is what happens when we await something.
As you can see in the first excerpt we simply await the method. But if we want to desugarize(!) it, we first start the operation and get a task promise to use in later time. We also get the current SynchronizationContext if it exist. Now the task continue executing, but at some point we need to execute the rest of our method. We do that by using
ContinueWith and passing a delegate for the rest of our method or our continuation block.
Next if the SynchronizationContext is null, then RestOfMethod() will be executed in the original TaskScheduler (which is often TaskScheduler.Default, meaning the ThreadPool). Note that we don’t run the continuation on the calling thread. Because we didn’t post the rest of our method to run on the captured context. It can be any thread if the SynchronizationContext is null, it could be a thread pool thread that runs it or a UI thread.
If the SynchronizationContext is not null, we pass the rest of our method to run on the original thread. That is the UI thread which invoked this task in the first place. We do it through passing a delegate and our method into post method of SynchronizationContext.
In this post I explained what
SynchronizationContext is and what problem it tries to solve. I also dig a little deeper into why we need this construct and how .Net deals with these issues under the hood.