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:
// ambient state captured into ec
ExecutionContext ec = ExecutionContext.Capture();
and it’s restored during the invocation of a delegate via the static run method:
ExecutionContext.Run(ec, delegate
{
… // code here will see ec’s state as ambient
}, null);
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:
public static void DoWork(Control c)
{
ThreadPool.QueueUserWorkItem(delegate
{
… // do work on ThreadPool
c.BeginInvoke(delegate
{
… // do work on UI
});
});
}
If I was instead writing my component to use SynchronizationContext, I might instead write it as:
public static void DoWork(SynchronizationContext sc)
{
ThreadPool.QueueUserWorkItem(delegate
{
… // do work on ThreadPool
sc.Post(delegate
{
… // do work on UI
}, null);
});
}
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):
public static void DoWork()
{
var sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(delegate
{
… // do work on ThreadPool
sc.Post(delegate
{
… // do work on the original context
}, null);
});
}
Flowing ExecutionContext vs Using SynchronizationContext
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:
private void button1_Click(object sender, EventArgs e)
{
button1.Text = await Task.Run(async delegate
{
string data = await DownloadAsync();
return Compute(data);
});
}
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.
Great article, thank you!
Wonderful job! Thank you. The reading was easy and very informative.
Wonderful post. I felt like as if I was reading a thriller novel, curious to know at each step what is going to happen next… I actually wanted to understand the implementation of AsyncLocal. But this article has helped me to understand a lot more than I was expecting. Thanks!!
Good post. Clarified a lot of things for me.