’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.
Read more: Parallel Programming with .NET