-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Cancellation patterns #52996
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
Merged
Merged
Cancellation patterns #52996
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
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
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
78 changes: 78 additions & 0 deletions
78
...ard/asynchronous-programming-patterns/cancel-non-cancelable-async-operations.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,78 @@ | ||
| --- | ||
| title: "Cancel non-cancelable async operations" | ||
| description: Learn practical patterns for cancellation when an async operation doesn't accept a cancellation token, including canceling the wait, canceling the operation, or both. | ||
| ms.date: 04/13/2026 | ||
| ai-usage: ai-assisted | ||
| dev_langs: | ||
| - "csharp" | ||
| - "vb" | ||
| helpviewer_keywords: | ||
| - "cancellation, async" | ||
| - "Task.WhenAny" | ||
| - "WithCancellation" | ||
| - "CancellationToken" | ||
| - "NetworkStream, cancellation" | ||
| --- | ||
| # Cancel non-cancelable async operations | ||
|
|
||
| Sometimes you need cancellation, but the operation you're waiting on doesn't accept a <xref:System.Threading.CancellationToken>. In that case, choose the behavior you need: | ||
|
|
||
| - Cancel the operation itself. | ||
| - Cancel only your wait. | ||
| - Cancel both the operation and your wait. | ||
|
|
||
| The right choice depends on who owns the operation and what cleanup guarantees you need. | ||
|
|
||
| ## Understand the three cancellation meanings | ||
|
|
||
| When people say, "cancel this async call," they usually mean one of three different things: | ||
|
|
||
| - **Cancel the operation.** Signal the operation to stop work. | ||
| - **Cancel the wait.** Stop awaiting and continue your workflow, even if the operation still runs. | ||
| - **Cancel both.** Request operation cancellation and also stop waiting promptly. | ||
|
|
||
| Treat these meanings as separate design decisions. If you mix them, cancellation behavior becomes hard to reason about. | ||
|
|
||
| ## Prefer token-aware APIs when available | ||
|
|
||
| Before you add a wrapper, check whether the API already supports cancellation tokens. Modern .NET APIs have much broader token support than older .NET Framework code. For example, many stream APIs in .NET now support cancellation, and `NetworkStream` async operations honor cancellation tokens. | ||
|
|
||
| Use token overloads whenever they exist: | ||
|
|
||
| :::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="PreferTokenAwareApis"::: | ||
| :::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="PreferTokenAwareApis"::: | ||
|
|
||
| ## Cancel only the wait by using `Task.WhenAny` | ||
|
|
||
| When an operation doesn't accept a token, cancel your wait by racing the operation against a token-backed task. This pattern often appears as a `WithCancellation` helper: | ||
|
|
||
| :::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="WithCancellation"::: | ||
| :::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="WithCancellation"::: | ||
|
|
||
| This pattern uses <xref:System.Threading.Tasks.Task.WhenAny*> to return as soon as either task completes. | ||
|
|
||
| Use this approach only when it's safe for the original operation to continue in the background. | ||
|
|
||
| ## Cancel both operation and wait when you own the operation | ||
|
|
||
| If you own the operation and it accepts a token, pass the token and still use a cancelable wait when needed: | ||
|
|
||
| :::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="CancelBoth"::: | ||
| :::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="CancelBoth"::: | ||
|
|
||
| This combination gives responsive cancellation for callers and cooperative shutdown for the underlying work. | ||
|
|
||
| ## Handle abandoned operations safely | ||
|
|
||
| If you cancel only the wait, the original task might later fault. Keep a reference so you can observe completion and log exceptions. Otherwise, you can miss failures and make troubleshooting harder. | ||
|
|
||
| :::code language="csharp" source="./snippets/cancel-non-cancelable-async-operations/csharp/Program.cs" id="ObserveLateFault"::: | ||
| :::code language="vb" source="./snippets/cancel-non-cancelable-async-operations/vb/Program.vb" id="ObserveLateFault"::: | ||
|
|
||
| ## See also | ||
|
|
||
| - [Consume the task-based asynchronous pattern](consuming-the-task-based-asynchronous-pattern.md) | ||
| - [Task cancellation](../parallel-programming/task-cancellation.md) | ||
| - [Cancellation in managed threads](../threading/cancellation-in-managed-threads.md) | ||
| - <xref:System.Threading.CancellationToken> | ||
| - <xref:System.Threading.Tasks.Task.WhenAny*> |
70 changes: 70 additions & 0 deletions
70
...asynchronous-programming-patterns/coalesce-cancellation-tokens-from-timeouts.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,70 @@ | ||
| --- | ||
| title: "Coalesce cancellation tokens from timeouts" | ||
| description: Learn how to combine caller-driven cancellation with timeout-driven cancellation by using linked cancellation token sources in modern .NET. | ||
| ms.date: 04/13/2026 | ||
| ai-usage: ai-assisted | ||
| dev_langs: | ||
| - "csharp" | ||
| - "vb" | ||
| helpviewer_keywords: | ||
| - "CancellationTokenSource.CreateLinkedTokenSource" | ||
| - "timeout, cancellation" | ||
| - "linked cancellation token" | ||
| - "CancelAfter" | ||
| - "WaitAsync" | ||
| --- | ||
| # Coalesce cancellation tokens from timeouts | ||
|
|
||
| Many APIs need two cancellation triggers: | ||
|
|
||
| - A caller-provided <xref:System.Threading.CancellationToken>. | ||
| - A timeout enforced by your component. | ||
|
|
||
| Use <xref:System.Threading.CancellationTokenSource.CreateLinkedTokenSource*> to merge both into a single token that your async work can consume. | ||
|
|
||
| ## Use a linked token source for caller token plus timeout | ||
|
|
||
| Create a timeout <xref:System.Threading.CancellationTokenSource>, link it with the caller token, and pass the linked token to the operation: | ||
|
|
||
| :::code language="csharp" source="./snippets/coalescing-cancellation-tokens-from-timeouts/csharp/Program.cs" id="LinkedTokenBasic"::: | ||
| :::code language="vb" source="./snippets/coalescing-cancellation-tokens-from-timeouts/vb/Program.vb" id="LinkedTokenBasic"::: | ||
|
|
||
| Dispose both `CancellationTokenSource` instances after the operation completes so registrations and timers are released promptly. | ||
|
|
||
| ## Encapsulate linking in a helper | ||
|
|
||
| Wrap linking logic in a helper method when several call sites need the same policy: | ||
|
|
||
| :::code language="csharp" source="./snippets/coalescing-cancellation-tokens-from-timeouts/csharp/Program.cs" id="LinkedTokenHelper"::: | ||
| :::code language="vb" source="./snippets/coalescing-cancellation-tokens-from-timeouts/vb/Program.vb" id="LinkedTokenHelper"::: | ||
|
|
||
| A helper keeps timeout behavior consistent and avoids duplicate disposal bugs. | ||
|
|
||
| ## Choose the right timeout model | ||
|
|
||
| Linked tokens are useful when the callee accepts a token and you need one unified cancellation path. In modern .NET, you also have these alternatives: | ||
|
|
||
| - Use <xref:System.Threading.CancellationTokenSource.CancelAfter*> to enforce timeouts on a `CancellationTokenSource` you own. | ||
| - Use <xref:System.Threading.Tasks.Task.WaitAsync*> when you want to time out the wait without canceling underlying work. | ||
|
|
||
| This example shows a `WaitAsync` timeout for "cancel wait only" semantics: | ||
|
|
||
| :::code language="csharp" source="./snippets/coalescing-cancellation-tokens-from-timeouts/csharp/Program.cs" id="WaitAsyncAlternative"::: | ||
| :::code language="vb" source="./snippets/coalescing-cancellation-tokens-from-timeouts/vb/Program.vb" id="WaitAsyncAlternative"::: | ||
|
|
||
| ## Summary | ||
|
|
||
| Use linked tokens when you need to combine caller intent and infrastructure timeouts into one cancellation contract. Keep ownership and disposal rules explicit: | ||
|
|
||
| - If your component creates a `CancellationTokenSource`, your component disposes it. | ||
| - If callers pass a token, never dispose the caller's token source. | ||
| - If an operation doesn't accept tokens, use wait-cancellation patterns instead of linked tokens. | ||
|
|
||
| ## See also | ||
|
|
||
| - [Cancel non-cancelable async operations](cancel-non-cancelable-async-operations.md) | ||
| - [Task cancellation](../parallel-programming/task-cancellation.md) | ||
| - [Cancellation in managed threads](../threading/cancellation-in-managed-threads.md) | ||
| - <xref:System.Threading.CancellationTokenSource.CreateLinkedTokenSource*> | ||
| - <xref:System.Threading.CancellationTokenSource.CancelAfter*> | ||
| - <xref:System.Threading.Tasks.Task.WaitAsync*> |
10 changes: 10 additions & 0 deletions
10
...s/cancel-non-cancelable-async-operations/csharp/CancelNonCancelableAsyncOperations.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> |
122 changes: 122 additions & 0 deletions
122
...us-programming-patterns/snippets/cancel-non-cancelable-async-operations/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,122 @@ | ||
| using System.Net.Sockets; | ||
|
|
||
| // <PreferTokenAwareApis> | ||
| public static class StreamExamples | ||
| { | ||
| public static async Task<int> ReadOnceAsync( | ||
| NetworkStream stream, | ||
| byte[] buffer, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| return await stream.ReadAsync( | ||
| buffer.AsMemory(0, buffer.Length), | ||
| cancellationToken); | ||
| } | ||
| } | ||
| // </PreferTokenAwareApis> | ||
|
|
||
| // <WithCancellation> | ||
| public static class TaskCancellationExtensions | ||
| { | ||
| public static async Task<T> WithCancellation<T>( | ||
| this Task<T> task, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| if (task.IsCompleted) | ||
| return await task.ConfigureAwait(false); | ||
|
|
||
| var cancellationTaskSource = new TaskCompletionSource<bool>( | ||
| TaskCreationOptions.RunContinuationsAsynchronously); | ||
|
|
||
| using var registration = cancellationToken.Register( | ||
| static state => | ||
| ((TaskCompletionSource<bool>)state!).TrySetResult(true), | ||
| cancellationTaskSource); | ||
|
|
||
| Task completed = await Task.WhenAny(task, cancellationTaskSource.Task) | ||
| .ConfigureAwait(false); | ||
|
|
||
| if (completed != task) | ||
| throw new OperationCanceledException(cancellationToken); | ||
|
|
||
| return await task.ConfigureAwait(false); | ||
| } | ||
| } | ||
| // </WithCancellation> | ||
|
|
||
| // <CancelBoth> | ||
| public static class CancelBothDemo | ||
| { | ||
| public static async Task<string> FetchDataAsync(CancellationToken cancellationToken) | ||
| { | ||
| await Task.Delay(500, cancellationToken); | ||
| return "payload"; | ||
| } | ||
|
|
||
| public static async Task RunAsync() | ||
| { | ||
| using var cts = new CancellationTokenSource(); | ||
| cts.CancelAfter(100); | ||
|
|
||
| try | ||
| { | ||
| string payload = await FetchDataAsync(cts.Token) | ||
| .WithCancellation(cts.Token); | ||
| Console.WriteLine(payload); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| Console.WriteLine("Canceled operation and wait."); | ||
| } | ||
| } | ||
| } | ||
| // </CancelBoth> | ||
|
|
||
| // <ObserveLateFault> | ||
| public static class ObserveLateFaultDemo | ||
| { | ||
| private static async Task<int> FaultLaterAsync() | ||
| { | ||
| await Task.Delay(250); | ||
| throw new InvalidOperationException("Background operation failed."); | ||
| } | ||
|
|
||
| public static async Task RunAsync() | ||
| { | ||
| Task<int> operation = FaultLaterAsync(); | ||
|
|
||
| using var cts = new CancellationTokenSource(50); | ||
|
|
||
| try | ||
| { | ||
| await operation.WithCancellation(cts.Token); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| Console.WriteLine("Stopped waiting; operation still running."); | ||
| } | ||
|
|
||
| _ = operation.ContinueWith( | ||
| t => Console.WriteLine($"Observed late fault: {t.Exception!.InnerException!.Message}"), | ||
| TaskContinuationOptions.OnlyOnFaulted); | ||
|
|
||
| await Task.Delay(300); | ||
| } | ||
| } | ||
| // </ObserveLateFault> | ||
|
|
||
| public class Program | ||
| { | ||
| public static async Task Main() | ||
| { | ||
| Console.WriteLine("=== Cancel both operation and wait ==="); | ||
| await CancelBothDemo.RunAsync(); | ||
|
|
||
| Console.WriteLine(); | ||
| Console.WriteLine("=== Cancel wait only and observe late fault ==="); | ||
| await ObserveLateFaultDemo.RunAsync(); | ||
|
|
||
| Console.WriteLine(); | ||
| Console.WriteLine("Done."); | ||
| } | ||
| } | ||
9 changes: 9 additions & 0 deletions
9
...ppets/cancel-non-cancelable-async-operations/vb/CancelNonCancelableAsyncOperations.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> | ||
| <RootNamespace>CancelNonCancelableAsyncOperations</RootNamespace> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| </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.
Uh oh!
There was an error while loading. Please reload this page.