Why did the async method break up with the cancellation token?
Because every time things got too intense, it just cancelled! 😆
In C#, the async
and await
keywords simplify asynchronous programming, but handling cancellations is critical in real-world applications. Users may close applications or cancel operations, and if left unmanaged, these cancellations can lead to unnecessary resource usage or application hang-ups. This article explores how cancellation tokens work within a C# async flow, best practices for implementing them, and examples for common use cases.
What Is a Cancellation Token?
A CancellationToken
is a struct in .NET that allows cooperative cancellation between threads. It signals a cancellation request, enabling tasks, methods, or operations to terminate cleanly without leaving tasks hanging. The CancellationTokenSource
class generates a CancellationToken
, allowing it to be canceled from a central place.
Why Use Cancellation Tokens?
Cancellation tokens serve several purposes in async code:
- Resource Efficiency: Allows your application to stop a task that’s no longer needed, freeing up resources.
- User Experience: Improves responsiveness by terminating lengthy tasks upon request.
- Controlled Execution: Lets developers define specific points where the code checks for cancellation, ensuring a clean termination.
Working with Cancellation Tokens
1. Creating and Passing a Cancellation Token
You can create a cancellation token using CancellationTokenSource
. Pass the token to async methods that support cancellation:
var cancellationTokenSource = new CancellationTokenSource();
var token = cancellationTokenSource.Token;
await PerformAsyncOperation(token);
2. Handling Cancellation in an Async Method
In a cancellable async method, you should periodically check for the token’s status using token.IsCancellationRequested
or call token.ThrowIfCancellationRequested()
at logical points:
public async Task PerformAsyncOperation(CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
// Check if cancellation was requested
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Operation was cancelled.");
return;
}
// Simulate work
await Task.Delay(100);
}
}
Or, you can use ThrowIfCancellationRequested()
to throw an OperationCanceledException
:
public async Task PerformAsyncOperation(CancellationToken cancellationToken)
{
for (int i = 0; i < 100; i++)
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(100);
}
}
3. Canceling the Operation
To cancel the operation, call Cancel()
on the CancellationTokenSource
:
cancellationTokenSource.Cancel();
This signals any method using the associated token to terminate if cancellation checks are implemented.
4. Handling Exceptions and Task Completion
When using ThrowIfCancellationRequested()
, a TaskCanceledException
or OperationCanceledException
is raised. You can handle this in a try-catch block to respond gracefully:
try
{
await PerformAsyncOperation(token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation was canceled successfully.");
}
Advanced Patterns and Scenarios
Timeout-Based Cancellations
You can set a timeout with CancellationTokenSource
for scenarios where operations should not exceed a specific duration:
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await PerformAsyncOperation(cancellationTokenSource.Token);
Linked Cancellation Tokens
To aggregate multiple cancellation requests, you can use CancellationTokenSource.CreateLinkedTokenSource
:
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
await PerformAsyncOperation(linkedCts.Token);
Making Long-Running Loops Cancellable
For async flows within loops, checking IsCancellationRequested
at each iteration is essential for long-running loops:
public async Task LongRunningLoop(CancellationToken token)
{
while (true)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100); // Perform some work
}
}
Best Practices
- Check for Cancellation Periodically: Don’t rely solely on
ThrowIfCancellationRequested()
; checkIsCancellationRequested
as a low-cost option for loops. - Avoid Catch-All Exception Blocks: Handle specific exceptions (
TaskCanceledException
,OperationCanceledException
) to respond to cancellations. - Dispose of CancellationTokenSource: It implements
IDisposable
and should be disposed of when no longer needed. - Use Cancellation Tokens Sparingly: Avoid overusing tokens in complex chains unless each method genuinely benefits from cancellation logic.
Integrating cancellation tokens in C# async code is an efficient way to improve performance, responsiveness, and control. By applying these principles, you can create robust, user-friendly applications that handle cancellations gracefully and avoid unnecessary resource consumption.