ExecutionContext vs SynchronizationContext

I’ve been asked a few times recently various questions about ExecutionContext and SynchronizationContext, for example what the differences are between them, what it means to “flow” them, and how they relate to the new async/await keywords in C# and Visual Basic. I thought I’d try to tackle some of those questions here.

WARNING: This post goes deep into an advanced area of .NET that most developers never need to think about.

What is ExecutionContext, and what does it mean to flow it?

ExecutionContext is one of those things that the vast majority of developers never need to think about. It’s kind of like air: it’s important that it’s there, but except at some crucial times (e.g. when something goes wrong with it), we don’t think about it being there. ExecutionContext is actually just a container for other contexts. Some of these other contexts are ancillary, while some are vital to the execution model of .NET, but they all follow the same philosophy I described for ExecutionContext: if you have to know they’re there, either you’re doing something super advanced, or something’s gone wrong.

ExecutionContext is all about “ambient” information, meaning that it stores data relevant to the current environment or “context” in which you’re running. In many systems, such ambient information is maintained in thread-local storage (TLS), such as in a ThreadStatic field or in a ThreadLocal<T>. In a synchronous world, such thread-local information is sufficient: everything’s happening on that one thread, and thus regardless of what stack frame you’re in on that thread, what function is being executed, and so forth, all code running on that thread can see and be influenced by data specific to that thread. For example, one of the contexts contained by ExecutionContext is SecurityContext, which maintains information like the current “principal” and information about code access security (CAS) denies and permits. Such information can be associated with the current thread, such that if one stack frame denies access to a certain permission and then calls into another method, that called method will still be subject to the denial set on the thread: when it tries to do something that needs that permission, the CLR will check the current thread’s denials to see if the operation is allowed, and it’ll find the data put there by the caller.

Things get more complicated when you move from a synchronous world to an asynchronous world. All of a sudden, TLS becomes largely irrelevant. In a synchronous world, if I do operation A, then operation B, and then operation C, all three of those operations happen on the same thread, and thus all three of those are subject to the ambient data stored on that thread. But in an asynchronous world, I might start A on one thread and have it complete on another, such that operation B may start or run on a different thread than A, and similarly such that C may start or run on a different thread than B. This means that this ambient context we’ve come to rely on for controlling details of our execution is no longer viable, because TLS doesn’t “flow” across these async points. Thread-local storage is specific to a thread, whereas these asynchronous operations aren’t tied to a specific thread. There is, however, typically a logical flow of control, and we want this ambient data to flow with that control flow, such that the ambient data moves from one thread to another. This is what ExecutionContext enables.

ExecutionContext is really just a state bag that can be used to capture all of this state from one thread and then restore it onto another thread while the logical flow of control continues. ExecutionContext is captured with the static Capture method:

All of the methods in the .NET Framework that fork asynchronous work capture and restore ExecutionContext in a manner like this (that is, all except for those prefixed with the word “Unsafe,” which are unsafe because they explicitly do not flow ExecutionContext). For example, when you use Task.Run, the call to Run captures the ExecutionContext from the invoking thread, storing that ExecutionContext instance into the Task object. When the delegate provided to Task.Run is later invoked as part of that Task’s execution, it’s done so via ExecutionContext.Run using the stored context. This is true for Task.Run, for ThreadPool.QueueUserWorkItem, for Delegate.BeginInvoke, for Stream.BeginRead, for DispatcherSynchronizationContext.Post, and for any other async API you can think of. All of them capture the ExecutionContext, store it, and then use the stored context later on during the invocation of some code.

When we talk about “flowing ExecutionContext,” we’re talking about exactly this process of taking the state that was ambient on one thread and restoring that state onto a thread at some later point while that thread executes a supplied delegate.

What is SynchronizationContext, and what does it mean to capture and use it?

We in software development love abstractions. We’re rarely happy hardcoding to a particular implementation; rather, when writing higher-level systems, we abstract away the details of a particular implementation so that we can plug in a different implementation later without having to change our higher-level system. This is why we have interfaces, this is why we have abstract classes, this is why we have virtual methods, and so on.

SynchronizationContext is just an abstraction, one that represents a particular environment you want to do some work in. As an example of such an environment, Windows Forms apps have a UI thread (while it’s possible for there to be multiple, for the purposes of this discussion it doesn’t matter), which is where any work that needs to use UI controls needs to happen. For cases where you’re running code on a ThreadPool thread and you need to marshal work back to the UI so that this work can muck with UI controls, Windows Forms provides the Control.BeginInvoke method. You give a delegate to a Control’s BeginInvoke method, and that delegate will be invoked back on the thread with which that control is associated.

So, if I’m writing a component that needs to schedule some work to the ThreadPool and then continue with some work back on the UI thread, I can code my component to use Control.BeginInvoke. But now what if I decide I want to use my component in a WPF app? WPF has the same UI thread constraint that Windows Forms has, but it has a different mechanism for marshaling back to the UI thread: rather than using Control.BeginInvoke on a control associated with the right thread, you use Dispatcher.BeginInvoke (or InvokeAsync) on the Dispatcher instance associated with the right thread.

We now have two different APIs for achieving the same basic operation, so how do I write my component to be agnostic of the UI framework? By using SynchronizationContext. SynchronizationContext provides a virtual Post method; this method simply takes a delegate and runs it wherever, whenever, and however the SynchronizationContext implementation deems fit. Windows Forms provides the WindowsFormSynchronizationContext type which overrides Post to call Control.BeginInvoke. WPF provides the DispatcherSynchronizationContext type which overrides Post to call Dispatcher.BeginInvoke. And so on. As such, I can now code my component to use SynchronizationContext instead of tying it to a specific framework.

If I was writing my component specifically to target Windows Forms, I might implement my go-to-the-ThreadPool-and-then-back-to-the-UI-thread logic something like the following:

Of course, it’s annoying (and prohibitive for certain desired programming models) to need to pass around the target context to come back to, so SynchronizationContext provides the Current property, which allows you to discover from the current thread the context that will let you get back to the current environment, if there is one. This allows you to “capture it” (i.e. read the reference from SynchronizationContext.Current and store that reference for later usage):

Now, we have a very important observation to make: flowing ExecutionContext is semantically very different than capturing and posting to a SynchronizationContext.

When you flow ExecutionContext, you’re capturing the state from one thread and then restoring that state such that it’s ambient during the supplied delegate’s execution. That’s not what happens when you capture and use a SynchronizationContext. The capturing part is the same, in that you’re grabbing data from the current thread, but you then use that state differently. Rather than making that state current during the invocation of the delegate, with SynchronizationContext.Post you’re simply using that captured state to invoke the delegate. Where and when and how that delegate runs is completely up to the implementation of the Post method.

How does this apply to async/await?

The framework support behind the async and await keywords automatically interacts with both ExecutionContext and SynchronizationContext.

Whenever code awaits an awaitable whose awaiter says it’s not yet complete (i.e. the awaiter’s IsCompleted returns false), the method needs to suspend, and it’ll resume via a continuation off of the awaiter. This is one of those asynchronous points I referred to earlier, and thus, ExecutionContext needs to flow from the code issuing the await through to the continuation delegate’s execution. That’s handled automatically by the Framework. When the async method is about to suspend, the infrastructure captures an ExecutionContext. The delegate that gets passed to the awaiter has a reference to this ExecutionContext instance and will use it when resuming the method. This is what enables the important “ambient” information represented by ExecutionContext to flow across awaits.

The Framework also has support for SynchronizationContext. The aforementioned support for ExecutionContext is built into the “builders” that represent async methods (e.g. System.Runtime.CompilerServices.AsyncTaskMethodBuilder), and these builders ensure that ExecutionContext is flowed across await points regardless of what kind of awaitable is being used. In contrast, support for SynchronizationContext is built into the support for awaiting Task and Task<TResult>, specifically. Custom awaiters could add similar logic themselves, but they don’t get it automatically; that’s by design, as being able to customize when and how the continuation gets invoked is part of why custom awaiters are useful.

When you await a task, by default the awaiter will capture the current SynchronizationContext, and if there was one, when the task completes it’ll Post the supplied continuation delegate back to that context, rather than running the delegate on whatever thread the task completed or rather than scheduling it to run on the ThreadPool. If a developer doesn’t want this marshaling behavior, it can be controlled by changing the awaitable/awaiter that’s used. Whereas this behavior is always employed when you await a Task or Task<TResult>, you can instead await the result of calling task.ConfigureAwait(…). The ConfigureAwait method returns an awaitable that enables this default marshaling behavior to be suppressed. Whether it’s suppressed is controlled by a Boolean passed to the ConfigureAwait method. If continueOnCapturedContext is true, then you get the default behavior; if it’s false, the awaiter doesn’t check for a SynchronizationContext, pretending as if there wasn’t one. (Note that when the awaited task completes, regardless of ConfigureAwait, the runtime may check the context that’s current on the resuming thread to determine whether it’s ok to synchronously run the continuation there or whether the continuation must be scheduled asynchronously from that point.)

Note that while ConfigureAwait provides explicit, await-related programming model support for changing behavior related to SynchronizationContext, there is no await-related programming model support for suppressing ExecutionContext flow. This is on purpose. ExecutionContext is not something developers writing async code should need to worry about; it’s infrastructure level support that helps to simulate synchronous semantics (i.e. TLS) in an asynchronous world. Most folks can and should completely ignore that it’s there (and should avoid using the ExecutionContext.SuppressFlow method unless they really know what you’re doing). In contrast, where code runs is something developers should be cognizant of, and thus SynchronizationContext rises to level of something that does deserve explicit programming model support. (In fact, as I’ve stated in other posts, most library implementers should consider using ConfigureAwait(false) on every await of a task.)

Isn’t SynchronizationContext part of ExecutionContext?

I’ve glossed over some details up until this point, but I can’t avoid them any further.

The main thing I glossed over is that of all the contexts ExecutionContext is capable of flowing (e.g. SecurityContext, HostExecutionContext, CallContext, etc.), SynchronizationContext is actually one of them. This is, I personally believe, a mistake in API design, one that’s caused a few problems since it was instituted in .NET many versions ago. Nevertheless, it’s the design we have and have had for a long time, and changing it now would be a breaking change.

When you call the public ExecutionContext.Capture() method, that checks for a current SynchronizationContext, and if there is one, it stores that into the returned ExecutionContext instance. Then, when the public ExecutionContext.Run method is used, that captured SynchronizationContext is restored as Current during the execution of the supplied delegate.

Why is this problematic? Flowing SynchronizationContext as part of ExecutionContext changes the meaning of SynchronizationContext.Current. SynchronizationContext.Current is supposed to be something you can access to get back to the environment that you’re currently in at the time you access Current, so if SynchronizationContext flows to be current on another thread, you can’t trust what SynchronizationContext.Current means. In such a case, it could either be the way to get back to the current environment, or it could be the way to get back to some environment that occurred at some point previously in the flow.

As one example of how this can be problematic, consider the following code:

Here’s what my mental model tells me will happen with this code. A user clicks button1, causing the UI framework to invoke button1_Click on the UI thread. The code then kicks off a work item to run on the ThreadPool (via Task.Run). That work item starts some download work and asynchronously waits for it to complete. A subsequent work item on the ThreadPool then does some compute-intensive operation on the result of that download, and returns the result, causing the Task that was being awaited on the UI thread to complete. At that point, the UI thread processes the remainder of this button1_Click method, storing the result of the computation into the button1’s Text property.

My expectation is valid if SynchronizationContext doesn’t flow as part of ExecutionContext. If it does flow, however, I will be sorely disappointed. Task.Run captures ExecutionContext when invoked, and uses it to run the delegate passed to it. That means that the UI SynchronizationContext which was current when Task.Run was invoked would flow into the Task and would be Current while invoking DownloadAsync and awaiting the resulting task. That then means that the await will see the Current SynchronizationContext and Post the remainder of asynchronous method as a continuation to run back on the UI thread. And that means my Compute method will very likely be running on the UI thread, not on the ThreadPool, causing responsiveness problems for my app.

The story now gets a bit messier: ExecutionContext actually has two Capture methods, but only one of them is public. The internal one (internal to mscorlib) is the one used by most asynchronous functionality exposed from mscorlib, and it optionally allows the caller to suppress the capturing of SynchronizationContext as part of ExecutionContext; corresponding to that, there’s also an internal overload of the Run method that supports ignoring a SynchronizationContext that’s stored in the ExecutionContext, in effect pretending one wasn’t captured (this is, again, the overload used by most functionality in mscorlib). What this means is that pretty much any asynchronous operation whose core implementation resides in mscorlib won’t flow SynchronizationContext as part of ExecutionContext, but any asynchronous operation whose core implementation resides anywhere else will flow SynchronizationContext as part of ExecutionContext. I previously mentioned that the “builders” for async methods were the types responsible for flowing ExecutionContext in async methods, and these builders do live in mscorlib, and they do use the internal overloads… as such, SynchronizationContext is not flowed as part of ExecutionContext across awaits (this, again, is separate from how task awaiters support capturing the SynchronizationContext and Post’ing back to it). To help deal with the cases where ExecutionContext does flow SynchronizationContext, the async method infrastructure tries to ignore SynchronizationContexts set as Current due to being flowed.

In short, SynchronizationContext.Current does not “flow” across await points.

Tags

Join the conversation

Nice post! I noted one mistake: when you say "My expectation is valid if SynchronizationContext doesn’t flow as part of SynchronizationContext", the last SynchronizationContext should be ExecutionContext. Just to be sure, is new Thread(…) considered an async point? It's a shame that asynchronous IO operations in the core libraries don't exhibit uniform behavior. Isn't it possible to make the overload public? (You could also use the InternalsVisibleToAttribute in mscorlib, but that isn't much of a solution.)

Jonathan, there are a variety of contexts that make up ExecutionContext. One of these is CallContext, which does allow you to store/flow arbitrary data, e.g. CallContext.LogicalSetData and CallContext.LogicalGetData.

where DoStuffAndUpdateUI() in turn does an await (for a webrequest download) then *updates the UI* without explicitly marshaling back to the UI thread. However, the UI update is marshaled back onto the UI thread.

Does this mean that, at least in the case of WinForms, that SynchronizationContext is flowing across awaits? e.g., it's using the public ExecutionContext.Capture() ?

The marshaling back to the UI thread is handled by the awaiter. The example is awaiting a Task from the UI thread, so when the Task completes, the awaiter Posts back to the SynchronizationContext that was current when the await was issued.

Ahh, that explains it. SynchronizationContext.Current doesn't flow across await points but is captured, the way local variables passed to a lambda are captured, at the moment the continuation is registered.

@kyann: There's no way today to modify the ExecutionContext.Capture logic. However, you can store / retrieve data that flows with the captured context by using CallContext.LogicalSetData / CallContext.LogicalGetData.

It would be interesting to see some more in-depth discussion of how to properly attach data to an execution context using LogicalCallContext.LogicalSetData / LogicalGetData

I recently was doing this to capture some application specific context data that needs to flow across thread boundaries. Context data like 'username' from a forms / custom authentication mechanism, internal id's, performance metrics, etc.. are all useful data to attach to a thread but it's so low-level that doing so is intimidating.

Using context like this for application code is an architectural nightmare (service locator), but trying to do parameter / constructor injection of stuff like the username when your code wasn't built out to do this originally is a nightmare too.

The LogicalCallContext is a Dictionary<object,object> but only performs a shallow copy on the key/value data stored inside this dictionary.

There is a special hardcoded case for using ICloneable when flowing the ETW ActivityID but otherwise nothing can be flowed using a deep copy.

It seems that it is possible that Logical Call Context data is flowed 'backwards' into an invoking process (or as a result of some other trickery going on) because, for example, when using LogicalCallContext in an MSTest unit test, if you leave a reference to an object of a custom type in the context when the test terminates, the object ends up in the mstest.exe process space, it attempts to resolve this type name, and if the dll that declares the type isn't readily available in the GAC or AssemblyLoadPath then mstest crashes.

There are also several references on the internet citing problems with the way LogicalCallContext is flowed between thread pool threads by IIS which is an outstanding concern that i chose to ignore because async / flowing threads in IIS is not something we are doing intentionally right now, the happy-path works for us, and this application can afford that kind of assumption.

Is the logical call context actually being guaranteed to flow data properly? Would an IIS threadpool not doing so be a bug? Are there other important risks to be aware of?

After dealing with all these problems i ended up having to create custom implementations of IDictionary<> / ISet<> / IList<> that use LogicalCallContext as a backing data store, so that the buckets are deep-copied when the context is flowed. Perhaps similar classes should end up in the BCL, or a more user-friendly way of performing a deep-copy should be provided…

Hey Eric! Sorry for not responding sooner, I missed your comment somehow and only just saw it. I could be wrong, but as far as I'm aware, other than the unfortunate naming similarity, they have basically nothing to do with one another.

How can the ExecutionContext get lost, I sometimes (not reproducible) get an InvalidOperationException during runtime, telling very cryptic, that the undone ExecutionContext after the operation doesn't match the current thread.

This was an excellent read. I don't much like "Magic" happening underneath my code, I do trust it, but still want to know what's happening – so I love reading these in depth explanations, and want to see more of them!

@plevin: Sorry, I was distracted while answering and gave a poor response. That should have said SynchronizationContext does not explicitly flow across the await of a task. But if the awaited task is already complete by the time you await it, you're going to keep executing on the same thread, so the SynchronizationContext that was set won't have changed. Further, if there is a SynchronizationContext, the await will pick it up and use it to execute the continuation, so the continuation will be executed back through whatever SynchronizationContext was current. Also remember that we try to execute continuations synchronously with awaits when possible, so in your example most of those Console.WriteLine calls are all happening synchronously as part of the same continuation chain, which is why they all have the same value… sprinkle some "await Task.Yield();" around to force work to happen asynchronously, and you should start to see some different numbers (though you're at the whim of the thread pool's scheduling, and it could still put all of the execution back onto the same thread).

This is beginning to feel like must-know info for anyone doing async. It seems like every other question on async comes with a caveat of how deadlocks can occur due to interactions with contexts. What functionally appears to be valid code, has no resource contentions, all considerations have been made for the interactions of parallel operations, etc. can still deadlock without an understanding of how contexts fit into the picture. In some scenarios it will try to use a deadlocked context for a continuation for a task that the developer expected to be running asynchronously in another "thread", so you would never expect it to deadlock with the parent unless you understood these details.

You have to be more diligent about code reviews of less-experienced programmers(even those who have a solid handle on older multi-threaded APIs) because they can easily introduce a deadlock in code that appears otherwise correct without the knowledge of this. Since async tends to spread through your code(and is suggested practice "Don’t block on Tasks; use async all the way down."), you can't really shield less experienced developers from this. Where-as before you usually had only certain code that had a need for parallelization and so only a small portion of your code base required experience with multi-threading.

Reminds me when you had to marshal calls to the UI thread when doing multi threaded programming in WinForms, but that was easier to understand/train. If you made a mistake there, you always got an exception that made it clear you needed to marshal the call when testing, rather than an elusive deadlock that may/may not happen until production.

@Alon Hagshury: Glad you liked the post. Can you help me understand why you think the last example would deadlock? And where are you suggesting using ConfigurateAwait(false)? The delegate passed to Task.Run is executed on a thread pool thread where there isn't a SynchronizationContext, so the await inside that delegate won't attempt to post a continuation back to the UI thread, and using ConfigureAwait(false) on the Task.Run would be problematic, as the continuation that tries to set button1.Text would very likely not be on the UI thread.

@slabarque: The btnDoSomething_Click method synchronously calls DoItAsync, which synchronously calls PerformServiceCall, which traces out PerformServiceCall1 before any awaits have been reached. As such, it's still on the original thread with the original SynchronizationContext.

@Ned Stoyanov: If you're able to use .NET 4.6 or if you're using the new .NET Core stack, CultureInfo.CurrentCulture will automatically flow across awaits, so you don't need to do anything special to make that happen. Prior to .NET 4.6, you need to do such flowing manually. You could put it in logical call context, but if you wanted operations to respect that flowed culture as current, you'd need to do the work to pull the culture out of the context and assign it to be current.