Bemærk
Adgang til denne side kræver godkendelse. Du kan prøve at logge på eller ændre mapper.
Adgang til denne side kræver godkendelse. Du kan prøve at ændre mapper.
When you work with async and await, two context types play important but very different roles: ExecutionContext and SynchronizationContext. You learn what each one does, how each one interacts with async/await, and why SynchronizationContext.Current doesn't flow across await points.
What is ExecutionContext?
ExecutionContext is a container for ambient state that flows with the logical control flow of your program. In a synchronous world, ambient information lives in thread-local storage (TLS), and all code running on a given thread sees that data. In an asynchronous world, a logical operation can start on one thread, suspend, and resume on a different thread. Thread-local data doesn't follow along automatically—ExecutionContext makes it follow.
How ExecutionContext flows
Capture ExecutionContext by using ExecutionContext.Capture(). Restore it during execution of a delegate by using ExecutionContext.Run:
static void ExecutionContextCaptureDemo()
{
// Capture the current ExecutionContext
ExecutionContext? ec = ExecutionContext.Capture();
// Later, run a delegate within that captured context
if (ec is not null)
{
ExecutionContext.Run(ec, _ =>
{
// Code here sees the ambient state from the point of capture
Console.WriteLine("Running inside captured ExecutionContext.");
}, null);
}
}
Sub ExecutionContextCaptureExample()
' Capture the current ExecutionContext
Dim ec As ExecutionContext = ExecutionContext.Capture()
' Later, run a delegate within that captured context
If ec IsNot Nothing Then
ExecutionContext.Run(ec,
Sub(state)
' Code here sees the ambient state from the point of capture
Console.WriteLine("Running inside captured ExecutionContext.")
End Sub, Nothing)
End If
End Sub
All asynchronous APIs in .NET that fork work—Run, QueueUserWorkItem, BeginRead, and others—capture ExecutionContext and use the stored context when invoking your callback. This process of capturing state on one thread and restoring it on another is what "flowing ExecutionContext" means.
What is SynchronizationContext?
SynchronizationContext is an abstraction that represents a target environment where you want work to run. Different UI frameworks provide their own implementations:
- Windows Forms provides
WindowsFormsSynchronizationContext, which overrides Post to callControl.BeginInvoke. - WPF provides
DispatcherSynchronizationContext, which overrides Post to callDispatcher.BeginInvoke. - ASP.NET (on .NET Framework) provided its own context that ensured
HttpContext.Currentwas available.
By using SynchronizationContext instead of framework-specific marshaling APIs, you can write components that work across UI frameworks:
static class SyncContextExample
{
public static void DoWork()
{
// Capture the current SynchronizationContext
SynchronizationContext? sc = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
// ... do work on the ThreadPool ...
if (sc is not null)
{
sc.Post(_ =>
{
// This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.");
}, null);
}
});
}
}
Class SyncContextExample
Public Shared Sub DoWork()
' Install a custom SynchronizationContext for demonstration
Dim customContext As New SimpleSynchronizationContext()
SynchronizationContext.SetSynchronizationContext(customContext)
' Capture the current SynchronizationContext
Dim sc As SynchronizationContext = SynchronizationContext.Current
ThreadPool.QueueUserWorkItem(
Sub(state)
' ... do work on the ThreadPool ...
If sc IsNot Nothing Then
sc.Post(
Sub(s)
' This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.")
End Sub, Nothing)
Else
Console.WriteLine("No SynchronizationContext was captured.")
End If
End Sub)
End Sub
End Class
' A minimal SynchronizationContext for demonstration purposes
Class SimpleSynchronizationContext
Inherits SynchronizationContext
Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
' Queue the callback to run on a thread pool thread
ThreadPool.QueueUserWorkItem(
Sub(s)
d(state)
End Sub)
End Sub
End Class
Capture a SynchronizationContext
When you capture a SynchronizationContext, you read the reference from SynchronizationContext.Current and store it for later use. You then call Post on the captured reference to schedule work back to that environment.
Flowing ExecutionContext vs. using SynchronizationContext
Although both mechanisms involve capturing state from a thread, they serve different purposes:
- Flowing ExecutionContext means capturing ambient state and making that same state current during a delegate's execution. The delegate runs wherever it ends up running—the state follows it.
- Using SynchronizationContext means capturing a scheduling target and using it to decide where a delegate executes. The captured context controls where the delegate runs.
In short: ExecutionContext answers "what environment should be visible?" while SynchronizationContext answers "where should the code run?"
How async/await interacts with both contexts
The async/await infrastructure interacts with both contexts automatically, but in different ways.
ExecutionContext always flows
Whenever an await suspends a method (because the awaiter's IsCompleted returns false), the infrastructure captures an ExecutionContext. When the method resumes, the continuation runs within the captured context. This behavior is built into the async method builders (for example, AsyncTaskMethodBuilder) and applies regardless of what kind of awaitable you use.
SuppressFlow() exists, but it isn't an await-specific switch like ConfigureAwait(false). It suppresses ExecutionContext capture for work that you queue while suppression is active. It doesn't provide a per-await programming-model option that tells the async method builders to skip restoring the captured ExecutionContext for a continuation. That design is intentional because ExecutionContext is infrastructure-level support that simulates thread-local semantics in an asynchronous world, and most developers never need to think about it.
Task awaiters capture SynchronizationContext
The awaiters for Task and Task<TResult> include support for SynchronizationContext. The async method builders don't include this support.
When you await a task:
- The awaiter checks SynchronizationContext.Current.
- If a context exists, the awaiter captures it.
- When the task completes, the continuation is posted back to that captured context instead of running on the completing thread or the thread pool.
This behavior is how await "brings you back to where you were". For example, resuming on the UI thread in a desktop application.
ConfigureAwait controls SynchronizationContext capture
If you don't want the marshaling behavior, call ConfigureAwait with false:
await task.ConfigureAwait(false);
When you set continueOnCapturedContext to false, the awaiter doesn't check for a SynchronizationContext and the continuation runs wherever the task completes (typically on a thread pool thread). Library authors should use ConfigureAwait(false) on every await unless the code specifically needs to resume on the captured context.
SynchronizationContext.Current doesn't flow across awaits
This point is the most important: SynchronizationContext.Current doesn't flow across await points. The async method builders in the runtime use internal overloads that explicitly suppress SynchronizationContext from flowing as part of ExecutionContext.
Why this matters
Technically, SynchronizationContext is one of the sub-contexts that ExecutionContext can contain. If it flowed as part of ExecutionContext, code executing on a thread pool thread might see a UI SynchronizationContext as Current, not because that thread is the UI thread, but because the context "leaked" via flow. That change would alter the meaning of SynchronizationContext.Current from "the environment I'm currently in" to "the environment that historically existed somewhere in the call chain."
The Task.Run example
Consider code that offloads work to the thread pool. The UI-thread behavior described here applies only when SynchronizationContext.Current is non-null, such as in a UI app:
static class TaskRunExample
{
public static async Task ProcessOnUIThread()
{
// This method is called from a thread with a SynchronizationContext.
// Task.Run offloads work to the thread pool.
string result = await Task.Run(async () =>
{
string data = await DownloadAsync();
// Compute runs on the thread pool, not the original context,
// because SynchronizationContext doesn't flow into Task.Run.
return Compute(data);
});
// Back on the original context (the continuation is posted back).
Console.WriteLine(result);
}
private static async Task<string> DownloadAsync()
{
await Task.Delay(100);
return "downloaded data";
}
private static string Compute(string data) =>
$"Computed: {data.Length} chars";
}
Class TaskRunExampleClass
Public Shared Async Function ProcessOnUIThread() As Task
' If a SynchronizationContext is present when this method starts,
' the outer await captures it. Task.Run still offloads work to the thread pool.
Dim result As String = Await Task.Run(
Async Function()
Dim data As String = Await DownloadAsync()
' Compute runs on the thread pool, not the caller's context,
' because SynchronizationContext doesn't flow into Task.Run.
Return Compute(data)
End Function)
' Resume on the captured context, if one was available.
Console.WriteLine(result)
End Function
Private Shared Async Function DownloadAsync() As Task(Of String)
Await Task.Delay(100)
Return "downloaded data"
End Function
Private Shared Function Compute(data As String) As String
Return $"Computed: {data.Length} chars"
End Function
End Class
In a console app, SynchronizationContext.Current is typically null, so the snippet doesn't resume on a real UI thread. Instead, the snippet illustrates the rule conceptually: if a UI SynchronizationContext flowed across await points, the await inside the delegate passed to Task.Run would see that UI context as Current. The continuation after await DownloadAsync() would then post back to the UI thread, causing Compute(data) to run on the UI thread instead of on the thread pool. That behavior defeats the purpose of the Task.Run call.
Because the runtime suppresses SynchronizationContext flow in ExecutionContext, the await inside Task.Run doesn't inherit an outer UI context, and the continuation keeps running on the thread pool as intended.
Summary
| Aspect | ExecutionContext | SynchronizationContext |
|---|---|---|
| Purpose | Carries ambient state across async boundaries | Represents a target scheduler (where code should run) |
| Captured by | Async method builders (infrastructure) | Task awaiters (await task) |
| Flows across await? | Yes, always | No—captured and posted to, not flowed |
| Suppression API | ExecutionContext.SuppressFlow (advanced; rarely needed) |
ConfigureAwait(false) |
| Scope | All awaitables | Task and Task<TResult> (custom awaiters can add similar logic) |