Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/navigate/advanced-programming/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ items:
href: ../../standard/asynchronous-programming-patterns/async-wrappers-for-synchronous-methods.md
- name: Synchronous wrappers for asynchronous methods
href: ../../standard/asynchronous-programming-patterns/synchronous-wrappers-for-asynchronous-methods.md
- name: ExecutionContext and SynchronizationContext
href: ../../standard/asynchronous-programming-patterns/executioncontext-synchronizationcontext.md
- name: SynchronizationContext and console apps
href: ../../standard/asynchronous-programming-patterns/synchronizationcontext-console-apps.md
- name: Event-based asynchronous pattern (EAP)
items:
- name: Documentation overview
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
title: "ExecutionContext and SynchronizationContext"
description: Learn about the difference between ExecutionContext and SynchronizationContext in .NET, how each one is used with async/await, and why SynchronizationContext.Current doesn't flow across awaits.
ms.date: 04/07/2026
ai-usage: ai-assisted
dev_langs:
- "csharp"
- "vb"
helpviewer_keywords:
- "ExecutionContext"
- "SynchronizationContext"
- "async await context"
- "ConfigureAwait"
- "context flow"
- "asynchronous programming, context"
---
# ExecutionContext and SynchronizationContext

When you work with `async` and `await`, two context types play important but very different roles: <xref:System.Threading.ExecutionContext> and <xref:System.Threading.SynchronizationContext>. This article explains what each one does, how it interacts with `async`/`await`, and why <xref:System.Threading.SynchronizationContext.Current?displayProperty=nameWithType> doesn't flow across await points.

## What is ExecutionContext?

<xref:System.Threading.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—<xref:System.Threading.ExecutionContext> makes it follow.

### How ExecutionContext flows

<xref:System.Threading.ExecutionContext> is captured with <xref:System.Threading.ExecutionContext.Capture?displayProperty=nameWithType> and restored during execution of a delegate via <xref:System.Threading.ExecutionContext.Run*?displayProperty=nameWithType>:

:::code language="csharp" source="./snippets/executioncontext-synchronizationcontext/csharp/Program.cs" id="ExecutionContextCapture":::
:::code language="vb" source="./snippets/executioncontext-synchronizationcontext/vb/Program.vb" id="ExecutionContextCapture":::

All asynchronous APIs in .NET that fork work—<xref:System.Threading.Tasks.Task.Run*>, <xref:System.Threading.ThreadPool.QueueUserWorkItem*>, <xref:System.IO.Stream.BeginRead*>, and others—capture <xref:System.Threading.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?

<xref:System.Threading.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 <xref:System.Threading.SynchronizationContext.Post*> to call `Control.BeginInvoke`.
- WPF provides `DispatcherSynchronizationContext`, which overrides <xref:System.Threading.SynchronizationContext.Post*> to call `Dispatcher.BeginInvoke`.
- ASP.NET (on .NET Framework) provided its own context that ensured `HttpContext.Current` was available.

By using <xref:System.Threading.SynchronizationContext> instead of framework-specific marshaling APIs, you can write components that work across UI frameworks:

:::code language="csharp" source="./snippets/executioncontext-synchronizationcontext/csharp/Program.cs" id="SyncContextUsage":::
:::code language="vb" source="./snippets/executioncontext-synchronizationcontext/vb/Program.vb" id="SyncContextUsage":::

### Capturing a SynchronizationContext

When you capture a <xref:System.Threading.SynchronizationContext>, you read the reference from <xref:System.Threading.SynchronizationContext.Current?displayProperty=nameWithType> and store it for later use. You then call <xref:System.Threading.SynchronizationContext.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: <xref:System.Threading.ExecutionContext> answers "what environment should be visible?" while <xref:System.Threading.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 <xref:System.Threading.ExecutionContext>. When the method resumes, the continuation runs within the captured context. This behavior is built into the async method builders (for example, <xref:System.Runtime.CompilerServices.AsyncTaskMethodBuilder>) and applies regardless of what kind of awaitable you use.

There's no programming-model support for suppressing <xref:System.Threading.ExecutionContext> flow across awaits. This is intentional—<xref:System.Threading.ExecutionContext> is infrastructure-level support that simulates thread-local semantics in an asynchronous world, and most developers never need to think about it.

### SynchronizationContext is captured by task awaiters

Support for <xref:System.Threading.SynchronizationContext> is built into the awaiters for <xref:System.Threading.Tasks.Task> and <xref:System.Threading.Tasks.Task%601>, not into the async method builders.

When you `await` a task:

1. The awaiter checks <xref:System.Threading.SynchronizationContext.Current?displayProperty=nameWithType>.
1. If a context exists, the awaiter captures it.
1. 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 <xref:System.Threading.Tasks.Task.ConfigureAwait*> with `false`:

```csharp
await task.ConfigureAwait(false);
```

With `continueOnCapturedContext` set to `false`, the awaiter doesn't check for a <xref:System.Threading.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 is the most important point: <xref:System.Threading.SynchronizationContext.Current?displayProperty=nameWithType> **doesn't flow** across await points. The async method builders in the runtime use internal overloads that explicitly suppress <xref:System.Threading.SynchronizationContext> from flowing as part of <xref:System.Threading.ExecutionContext>.

### Why this matters

<xref:System.Threading.SynchronizationContext> is technically one of the sub-contexts that <xref:System.Threading.ExecutionContext> can contain. If it flowed as part of <xref:System.Threading.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 would change the meaning of <xref:System.Threading.SynchronizationContext.Current?displayProperty=nameWithType> 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 from a UI thread:

:::code language="csharp" source="./snippets/executioncontext-synchronizationcontext/csharp/Program.cs" id="TaskRunExample":::
:::code language="vb" source="./snippets/executioncontext-synchronizationcontext/vb/Program.vb" id="TaskRunExample":::

If <xref:System.Threading.SynchronizationContext> flowed across `await` points, the `await` inside the delegate passed to <xref:System.Threading.Tasks.Task.Run*?displayProperty=nameWithType> would see the 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 defeats the purpose of the `Task.Run` call.

Because the runtime suppresses <xref:System.Threading.SynchronizationContext> flow in <xref:System.Threading.ExecutionContext>, the `await` inside `Task.Run` doesn't see the UI context and the continuation runs 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) |

## See also

- [Task-based asynchronous pattern (TAP)](task-based-asynchronous-pattern-tap.md)
- [Consume the task-based asynchronous pattern](consuming-the-task-based-asynchronous-pattern.md)
- [SynchronizationContext and console apps](synchronizationcontext-console-apps.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System.Collections.Concurrent;

// Verification entry point
ExecutionContextCaptureDemo();
await TaskRunExample.ProcessOnUIThread();
SyncContextExample.DoWork();
Comment on lines +4 to +10
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TaskRunExample.ProcessOnUIThread() is awaited before the demo installs any SynchronizationContext (and no message pump is running). That means the snippet can’t actually demonstrate resuming to a UI-like context, and later comments that refer to a “UI thread” are misleading. Install/pump a demo SynchronizationContext around the call, or rewrite the example so it doesn’t claim UI-thread affinity.

Copilot uses AI. Check for mistakes.
await Task.Delay(200);
Console.WriteLine("Done.");

// <ExecutionContextCapture>
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);
}
}
// </ExecutionContextCapture>

// <SyncContextUsage>
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);
}
});
}
}
// </SyncContextUsage>

// <TaskRunExample>
static class TaskRunExample
{
public static async Task ProcessOnUIThread()
{
// Assume this method is called from a UI thread.
// 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 UI thread,
// because SynchronizationContext doesn't flow into Task.Run.
return Compute(data);
});

// Back on the UI thread (captured by the outer await).
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says the continuation is “Back on the UI thread,” but this snippet doesn’t run under a UI SynchronizationContext as written. Update the comment (for example, “back on the captured context, if any”) or restructure the demo to actually install and pump a single-threaded context.

Suggested change
// Back on the UI thread (captured by the outer await).
// Back on the captured context, if any.

Copilot uses AI. Check for mistakes.
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";
}
// </TaskRunExample>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>ExecutionContextAndSyncContext</RootNamespace>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
Imports System.Collections.Concurrent
Imports System.Threading

Module Program
Sub Main()
ExecutionContextCaptureExample()

SyncContextExample.DoWork()

Task.Run(Async Function()
Await TaskRunExampleClass.ProcessOnUIThread()
End Function).Wait()

Comment on lines +9 to +12
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TaskRunExampleClass.ProcessOnUIThread() is executed from inside Task.Run, so SynchronizationContext.Current is null at method entry. That means the “outer await” can’t capture a UI context, and the snippet won’t resume on a UI thread as the comments describe. Call the method on a thread that has an installed/pumped SynchronizationContext, or rewrite the comments so they match console behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +12
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TaskRunExampleClass.ProcessOnUIThread() is invoked from inside Task.Run, so SynchronizationContext.Current is null at method entry. That prevents the “outer await” from capturing any UI context, so the example doesn’t match the UI-thread behavior described in comments. Call the method from a thread with an installed/pumped SynchronizationContext, or update the comments to reflect console behavior.

Copilot uses AI. Check for mistakes.
Thread.Sleep(200)
Console.WriteLine("Done.")
End Sub

' <ExecutionContextCapture>
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
' </ExecutionContextCapture>
End Module

' <SyncContextUsage>
Class SyncContextExample
Public Shared Sub DoWork()
' 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.")
Comment on lines +51 to +52
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleSynchronizationContext.Post queues work to the thread pool, so the continuation does not run on the “original context (e.g. UI thread)” as the comment states. Either implement a single-threaded/pumped context for the demo, or update the comment to describe the actual behavior (callback runs on a thread pool thread).

Suggested change
' This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.")
' This callback runs on a thread pool thread because
' SimpleSynchronizationContext.Post queues work there.
Console.WriteLine("Callback ran on a thread pool thread.")

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +52
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SimpleSynchronizationContext.Post queues callbacks to the thread pool, so the posted delegate does not run on the “original context (e.g. UI thread)” as the comment states. Either change the demo context to one that actually enforces thread affinity (with a pump), or reword the comment to match the implementation.

Suggested change
' This runs on the original context (e.g. UI thread)
Console.WriteLine("Back on the original context.")
' This runs asynchronously via the captured SynchronizationContext.
' In this sample, SimpleSynchronizationContext queues work to the ThreadPool.
Console.WriteLine("Posted back through the captured SynchronizationContext.")

Copilot uses AI. Check for mistakes.
End Sub, Nothing)
End If
End Sub)
End Sub
End Class
' </SyncContextUsage>

' <TaskRunExample>
Class TaskRunExampleClass
Public Shared Async Function ProcessOnUIThread() As Task
' Assume this method is called from a UI thread.
' Task.Run 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 UI thread,
' because SynchronizationContext doesn't flow into Task.Run.
Return Compute(data)
End Function)

' Back on the UI thread (captured by the outer await).
Comment on lines +78 to +88
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment says execution resumes “Back on the UI thread,” but no UI-like SynchronizationContext is installed for this method call in the snippet. Update the comment to avoid claiming UI-thread resumption, or run the example under a message-pumped context so the statement becomes true.

Suggested change
' Assume this method is called from a UI thread.
' Task.Run 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 UI thread,
' because SynchronizationContext doesn't flow into Task.Run.
Return Compute(data)
End Function)
' Back on the UI thread (captured by the outer await).
' 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.

Copilot uses AI. Check for mistakes.
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
' </TaskRunExample>
Loading
Loading