-
Notifications
You must be signed in to change notification settings - Fork 6.1k
ExecutionContext and SynchronizationContext deep dive #52890
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
BillWagner
wants to merge
10
commits into
dotnet:main
Choose a base branch
from
BillWagner:async-aweigh-2
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
8055517
ExecutionContext and SynchronizationContext deep dive
BillWagner 17f8935
fix build suggestions
BillWagner 4114bb1
Apply suggestions from code review
BillWagner 5f5f980
Full edit pass
BillWagner 15de870
Merge branch 'async-aweigh-2' of https://github.com/BillWagner/docs i…
BillWagner 2253026
continue the merge
BillWagner 89fe908
Final edit pass
BillWagner 33bc2e4
fix build issues
BillWagner 2af0cbd
Apply suggestions from code review
BillWagner 3fda2df
Fix samles.
BillWagner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
...rd/asynchronous-programming-patterns/executioncontext-synchronizationcontext.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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/08/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>. You learn what each one does, how each one 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 | ||||||
|
|
||||||
| Capture <xref:System.Threading.ExecutionContext> by using <xref:System.Threading.ExecutionContext.Capture?displayProperty=nameWithType>. Restore it during execution of a delegate by using <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 | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| 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. | ||||||
|
|
||||||
| <xref:System.Threading.ExecutionContext.SuppressFlow> exists, but it isn't an await-specific switch like `ConfigureAwait(false)`. It suppresses <xref:System.Threading.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 <xref:System.Threading.ExecutionContext> for a continuation. That design is intentional because <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. | ||||||
|
|
||||||
| ### Task awaiters capture SynchronizationContext | ||||||
|
|
||||||
| The awaiters for <xref:System.Threading.Tasks.Task> and <xref:System.Threading.Tasks.Task%601> include support for <xref:System.Threading.SynchronizationContext>. The async method builders don't include this support. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
|
||||||
| 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); | ||||||
| ``` | ||||||
|
|
||||||
| When you set `continueOnCapturedContext` 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 point is the most important: <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 | ||||||
|
|
||||||
| Technically, <xref:System.Threading.SynchronizationContext> is 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 change would alter 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. The UI-thread behavior described here applies only when <xref:System.Threading.SynchronizationContext.Current?displayProperty=nameWithType> is non-null, such as in a UI app: | ||||||
|
|
||||||
| :::code language="csharp" source="./snippets/executioncontext-synchronizationcontext/csharp/Program.cs" id="TaskRunExample"::: | ||||||
| :::code language="vb" source="./snippets/executioncontext-synchronizationcontext/vb/Program.vb" id="TaskRunExample"::: | ||||||
|
|
||||||
| In a console app, <xref:System.Threading.SynchronizationContext.Current?displayProperty=nameWithType> is typically `null`, so the snippet doesn't resume on a real UI thread. Instead, the snippet illustrates the rule conceptually: if a UI <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 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 <xref:System.Threading.SynchronizationContext> flow in <xref:System.Threading.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) | | ||||||
|
|
||||||
| ## 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) | ||||||
10 changes: 10 additions & 0 deletions
10
...pets/executioncontext-synchronizationcontext/csharp/ExecutionContextAndSyncContext.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
150 changes: 150 additions & 0 deletions
150
...s-programming-patterns/snippets/executioncontext-synchronizationcontext/csharp/Program.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| using System.Collections.Concurrent; | ||
|
|
||
| // Verification entry point | ||
| ExecutionContextCaptureDemo(); | ||
| await TaskRunExample.ProcessOnUIThread(); | ||
|
|
||
| // Install a custom SynchronizationContext for the demo | ||
| var demoContext = new DemoSynchronizationContext(); | ||
| SynchronizationContext.SetSynchronizationContext(demoContext); | ||
| SyncContextExample.DoWork(); | ||
BillWagner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| demoContext.ProcessQueue(); | ||
| SynchronizationContext.SetSynchronizationContext(null); | ||
|
|
||
| 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> | ||
|
|
||
| static class SingleThreadSynchronizationContext | ||
| { | ||
| public static Task Run(Func<Task> asyncAction) | ||
| { | ||
| var previousContext = SynchronizationContext.Current; | ||
| var context = new SingleThreadContext(); | ||
| SynchronizationContext.SetSynchronizationContext(context); | ||
|
|
||
| Task task; | ||
| try | ||
| { | ||
| task = asyncAction(); | ||
| task.ContinueWith(_ => context.Complete(), TaskScheduler.Default); | ||
| context.RunOnCurrentThread(); | ||
| return task; | ||
| } | ||
| finally | ||
| { | ||
| SynchronizationContext.SetSynchronizationContext(previousContext); | ||
| } | ||
| } | ||
|
|
||
| private sealed class SingleThreadContext : SynchronizationContext | ||
| { | ||
| private readonly BlockingCollection<(SendOrPostCallback Callback, object? State)> _queue = new(); | ||
|
|
||
| public override void Post(SendOrPostCallback d, object? state) => _queue.Add((d, state)); | ||
|
|
||
| public void RunOnCurrentThread() | ||
| { | ||
| foreach (var workItem in _queue.GetConsumingEnumerable()) | ||
| { | ||
| workItem.Callback(workItem.State); | ||
| } | ||
| } | ||
|
|
||
| public void Complete() => _queue.CompleteAdding(); | ||
| } | ||
| } | ||
|
|
||
| // <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); | ||
| } | ||
| }); | ||
| } | ||
BillWagner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| // </SyncContextUsage> | ||
|
|
||
| // Minimal SynchronizationContext for demo purposes | ||
| sealed class DemoSynchronizationContext : SynchronizationContext | ||
| { | ||
| private readonly BlockingCollection<(SendOrPostCallback, object?)> _queue = new(); | ||
|
|
||
| public override void Post(SendOrPostCallback d, object? state) | ||
| { | ||
| _queue.Add((d, state)); | ||
| } | ||
|
|
||
| public void ProcessQueue() | ||
| { | ||
| var (callback, state) = _queue.Take(); | ||
| callback(state); | ||
|
|
||
| while (_queue.TryTake(out var workItem)) | ||
| { | ||
| workItem.Item1(workItem.Item2); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // <TaskRunExample> | ||
| 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"; | ||
| } | ||
| // </TaskRunExample> | ||
9 changes: 9 additions & 0 deletions
9
...snippets/executioncontext-synchronizationcontext/vb/ExecutionContextAndSyncContext.vbproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is use of async/await appropriate for VB devs?