Inner Mechanisms of Synchronization Context in .NET

Nakib
.NET Under the hood
8 min readMar 15, 2024
Photo by Max Duzij on Unsplash

This is the third blog post of the Unveiling Asynchronous & Parallel Programming in .NET Blog Series.

The first two posts are the prerequisite of this post. So please go back and read them first if you haven’t.

OK, let’s start….

The Problem:

Before diving deep into the Synchronization Context let’s discuss a common problem scenario.

Almost all of us have worked with UI applications at some point in our life.

Generally, the UI frameworks follow a pattern for handling UI elements. That is, they maintain some UI threads. And,

The UI elements can only be accessible from those UI threads.

For example, .NET Desktop Frameworks(like WinUI, WPF, etc.) have one or a few UI threads. The same is true for Frameworks of other languages too. The UI elements/Bindings can only be accessed from those UI threads(s). So,

Tasks needing access or changing UI elements/Bindings must be executed from a UI thread.

Why UI threads?

Now, one question may have come to your mind: why are the UI elements/Bindings only accessible from the UI threads?

They are just .NET objects, right?

So why they aren’t made available for all threads?

Well, this question applies not only to .NET but to any UI application Framework of any language. Many frameworks follow the pattern of a single UI thread, where all the UI-related code is executed.

For example, Java Swing and AWT frameworks have a special thread called Event-Dispatching Thread that executes UI-related tasks.

The reason for using this single-threaded pattern is mainly thread safety. A multithreading environment where all the threads can manipulate the UI Concurrently/Parallelly will create many issues, and handling them with a locking mechanism is also cumbersome.

That’s why this pattern is used.

The .NET’s way to implement the UI thread pattern, Synchronization Context:

.NET awesomely implemented the UI thread pattern with a catch: the UI framework isn’t forced to use only a single UI thread. There can be multiple UI threads, and the UI framework is responsible for Thread Safety between those UI threads if they have multiple thread(s).

The developers who use the framework don’t need to know how many UI threads are in the framework or if such threads exist. All they need to know is the UI’s Synchronization Context and use it to schedule UI-related tasks there.

What is Synchronization Context?

Synchronization Context defines an Environment/Context(containing one or more thread(s)) where we can schedule tasks. The tasks are then executed by the Environment’s thread(s).

For example, if you are developing a WPF/WinUI application, you don’t need to know how many UI threads there are. All you have to do is schedule the UI tasks to the UI Synchronization Context.

How many UI threads are there or how to execute the UI tasks on those threads in a safe way are the concerns of WPF/WinUI, not you.

If we relate this to Java, we see that in Java Swing or AWT, we are scheduling tasks directly on the UI thread/Event-Dispatching thread, whereas in .NET, we aren’t handling the task on a thread now; rather, we are scheduling it on a Context(Synchronization Context).

Here, the Synchronization Context is abstracting away the UI thread. In the OOP world, we love abstraction. Because of the abstraction, the UI frameworks can have multiple threads.

Other benefits of the Abstraction:

This abstraction has some other benefits.

For example, a 3rd party library that wants to schedule some tasks in a UI thread doesn’t even need to know which UI framework the app uses; it can just grab the UI Synchronization Context and schedule tasks to the UI threads. Here, the Synchronization Context is abstracting away the whole UI framework.

Of course, UI frameworks like Win Forms and WPF have their specific method of scheduling tasks for their UI threads.

However, if we use Synchronization Context abstraction to schedule tasks, we can easily migrate from one code to another since our code is UI framework Agnostic.

For a better understanding, I would like you to read Stephen Toub’s “ExecutionContext vs SynchronizationContext.” For now, you may just read the section “What is SynchronizationContext, and what does it mean to capture and use it?

You may read the whole article later. I will add it to the Reference.

Synchronization Context as Task Scheduler:

When tasks are scheduled for Synchronization Context, the UI framework will execute them in a UI thread. Synchronization Context schedules tasks but it is not a child of the TaskScheduler class.

However, if you want a TaskScheduler to schedule tasks to the current UI thread, you can get it using TaskScheduler.FromCurrentSynchronizationContext().

One more important thing to note is,

If some code executing in a Synchronization Context creates tasks, those created tasks will be Scheduled in the same Synchronization Context by default.

Execution Context vs Synchronization Context:

Execution Context is related to Data. Two threads with the same Execution Context have access to the same Data. You can replicate this context(Execution Context) by loading the Context’s data to any thread.

The Execution Context in .NET does the same. It loads some data in the thread before task Execution to create the Context for the task to execute.

In short, The Execution Context Class in .NET is just a State bag that contains the Context’s Data.

But that is not the case with Synchronization Context.

Synchronization Context defines a non-replicable environment, not some data. That’s why you can’t replicate this environment in other threads.

There is only one instance of that Environment/Context; if you want to do something in that Environment, you need to go to that Environment to do this. You can’t just replicate/Recreate the environment in other threads as we do in the Execution Context.

The SynchronizationContext class in .NET is a Scheduler of that environment.

The Execution Context is a state bag containing some data, and the Synchronization Context is a Scheduler where you can schedule tasks to be executed.

Using Synchronization Context:

Don’t Block the UI thread:

When working with UI threads in any application, we should remember not to block the UI threads with non-UI-related tasks. Blocking the UI thread will hamper the UI experience badly.

We should use non-UI threads for non-UI tasks like Business logic, Calculations, or other CPU-intensive tasks.

Only the UI-related tasks like accessing and updating the UI should be scheduled on the UI threads.

In some specific scenarios, .NET also helps us to achieve this.

Say you have scheduled a task in Synchronization Context, and the code is executing like this.

Here, the CPU-intensive codes will be executed on the UI thread.

But say the CPU Intensive code after the await doesn’t need to be executed on the UI thread.

In that case, you can use the rescuer ConfigureAwait(false)

ConfigureAwait(false):

When you await on a task using ConfigureAwait(false), the following code after that await won’t be scheduled on the Current SynchronizationContext.

In our case, the Context is the UI Context/Thread.

In the code above, the code after the await won’t be scheduled on the UI thread. [I have described how awaits are converted to tasks in the first blog].

But if the GetDataFromDatabase() task is already completed(which is highly unlikely for an I/O operation), the code after the await will then be executed on the Current UI thread.

Because already completed tasks are treated like synchronous code, there is nothing to queue.

UI thread Deadlock:

When working in the UI thread, we should ensure no possibility of deadlock.

There will be a limited number of UI threads, so if we block I/O calls on a UI thread, there is a possibility of a Deadlock.

Let’s see an example,

Say, we have made a blocking call to a method ABCMethodAsync() in a UI thread, and there is only a single UI thread in the application.

ABCMethodAsync().Result;

We know, .Wait(), .Result, etc, blocks the current thread. So here the UI thread is blocked until the task returned from ABCMethodAsync() is complete.

Say, inside the ABCMethodAsync(), a method from a service is called for I/O and awaited(See the image of “Understanding Async/Await with tasks” section of the first blog for code).

If we recall from the First blog, the await points create multiple tasks.

Here, the ABCMethodAsync will create multiple tasks internally.

Since it is executing in a Synchronization Context, all the tasks will also be scheduled in the Synchronization Context.

However, since the Synchronization Context has only one UI thread and is already blocked, those internal tasks can’t be executed and will never be finished.

And the ABCMethodAsync().Result also won’t return until these internal tasks are finished.

So it is a Deadlock!!!

However, if there is more than one UI thread, the internally created tasks of ABCMethodAsync can be executed on the other thread, and the deadlock will not occur.

But deadlock will still happen if two such blocking calls are done.

There are two ways to solve this issue:

  1. Using await instead of the blocking call(.Result).
  2. Using ConfigureAwait(false) on the I/O call inside AbcMethodAsync will ensure that the tasks after the await points aren’t scheduled on Synchronization Context, rather they will be scheduled on the default Task Scheduler, so no deadlock happens.

We should always use await anyway, which solves the problem.

And if the Codes after an await don’t need to be executed on the UI thread, we should use ConfigureAwait(false).

Most importantly, if we are some library developer, we should always use ConfigureAwait(false) whenever possible(i.e., When the code after awaiting a task doesn’t need to be executed on the current Synchronization Context).

If you like my Content, You may support me at the link below

Synchronization Context in Non-UI Frameworks:

Almost all the blogs/tutorials give UI application examples when discussing Synchronization Context. For this reason, a common misconception is there that the Synchronization context is only applicable to UI Frameworks.

That’s not true.

Think again, Synchronization Context represents an Environment(Context). It can be anything.

In layman’s terms, it is just a Scheduler of an environment(with a few threads) where we schedule tasks. The Environment shouldn’t necessarily be UI environments and the threads shouldn’t necessarily be UI threads.

In this way, non-UI libraries can also have Synchronization Context.

Even we can create Custom Synchronization Contexts on our application. You can just create one or more dedicated threads with a Custom Synchronization Context and then execute the scheduled tasks on those threads.

Reference:

https://devblogs.microsoft.com/pfxteam/executioncontext-vs-synchronizationcontext/

--

--

Nakib
.NET Under the hood

Software Engineer, Currently working in Angular/.NET stack. Language/Tool Agnostic, strongly focuses on the fundamentals, constant learner.