.NET Cancellation Tokens
Introduction
When working with asynchronous operations in .NET, it's often necessary to cancel operations that are taking too long or are no longer needed. This is where cancellation tokens come into play. Cancellation tokens provide a standardized way to communicate cancellation requests across multiple components and threads in your application.
In this tutorial, you'll learn:
- What cancellation tokens are and why they're important
- How to create and use cancellation tokens
- Best practices for implementing cancellation in your code
Understanding Cancellation Tokens
What is a Cancellation Token?
A cancellation token is an object that represents a request to cancel one or more operations. In .NET, cancellation tokens are implemented through two primary classes:
CancellationToken
: A struct that represents a cancellation notificationCancellationTokenSource
: A class that creates and manages cancellation tokens
The relationship is straightforward: you create a CancellationTokenSource
, get a CancellationToken
from it, and pass that token to operations that support cancellation.
Creating and Using Cancellation Tokens
Let's start with a basic example:
using System;
using System.Threading;
using System.Threading.Tasks;
public class Program
{
public static async Task Main()
{
// Create a cancellation token source
CancellationTokenSource cts = new CancellationTokenSource();
// Get a token from the source
CancellationToken token = cts.Token;
// Start a task that accepts the cancellation token
Task longRunningTask = LongRunningOperationAsync(token);
// Simulate waiting for 2 seconds and then canceling
await Task.Delay(2000);
Console.WriteLine("Canceling the operation...");
cts.Cancel();
try
{
await longRunningTask;
Console.WriteLine("The task completed successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("The task was canceled.");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
Console.WriteLine("Main method complete.");
}
static async Task LongRunningOperationAsync(CancellationToken cancellationToken)
{
Console.WriteLine("Long running operation started...");
// Simulate work
for (int i = 0; i < 10; i++)
{
// Check if cancellation was requested
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"Working... {i + 1}/10");
await Task.Delay(1000, cancellationToken);
}
Console.WriteLine("Long running operation completed.");
}
}
Output:
Long running operation started...
Working... 1/10
Working... 2/10
Canceling the operation...
The task was canceled.
Main method complete.
Breaking Down the Example
- We create a
CancellationTokenSource
usingnew CancellationTokenSource()
. - We get a token from that source using the
Token
property. - We pass this token to our
LongRunningOperationAsync
method. - After 2 seconds, we call
cts.Cancel()
to request cancellation. - In our long-running operation, we periodically check for cancellation by calling
cancellationToken.ThrowIfCancellationRequested()
. - We also pass the token to
Task.Delay()
which supports cancellation.
Handling Cancellation in Different Scenarios
Cancellation with Timeout
You can set a cancellation token to automatically cancel after a specified timeout:
// Cancel after 5 seconds
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
// Or
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(5000); // 5000 milliseconds
Combining Multiple Cancellation Tokens
Sometimes you need to cancel an operation if any one of several conditions occurs:
CancellationTokenSource cts1 = new CancellationTokenSource();
CancellationTokenSource cts2 = new CancellationTokenSource();
// Create a linked token source that will be canceled when either source is canceled
using (CancellationTokenSource linkedCts =
CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token))
{
// Use linkedCts.Token for operations that should be canceled if either source is canceled
Task longRunningTask = LongRunningOperationAsync(linkedCts.Token);
// Later, cancel one of the sources
cts1.Cancel();
// The linkedCts will also be canceled
}
Manually Checking Cancellation Status
Sometimes, instead of throwing an exception, you might want to check if cancellation has been requested:
static async Task ProcessDataAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 1000; i++)
{
// Check cancellation without throwing
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Cancellation requested, cleaning up...");
// Perform cleanup operations
return;
}
// Process data
await ProcessItemAsync(i);
}
}
Real-World Example: Implementing a Cancellable Web API Request
Here's a more practical example showing how to implement cancellation in a Web API client:
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
public class WeatherService
{
private readonly HttpClient _httpClient;
public WeatherService()
{
_httpClient = new HttpClient();
}
public async Task<string> GetWeatherForecastAsync(string city, CancellationToken cancellationToken = default)
{
try
{
string url = $"https://api.example.com/weather?city={city}";
// The HttpClient methods accept a cancellation token
HttpResponseMessage response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
// ReadAsStringAsync also accepts a cancellation token
return await response.Content.ReadAsStringAsync(cancellationToken);
}
catch (OperationCanceledException)
{
Console.WriteLine("Weather request was canceled.");
throw; // Re-throw to let the caller handle it
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Weather request failed: {ex.Message}");
throw;
}
}
}
// Usage example:
public async Task GetWeatherWithTimeoutAsync()
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
try
{
var weatherService = new WeatherService();
string forecast = await weatherService.GetWeatherForecastAsync("Seattle", cts.Token);
Console.WriteLine($"Weather forecast: {forecast}");
}
catch (OperationCanceledException)
{
Console.WriteLine("The weather request timed out after 5 seconds.");
}
}
}
Best Practices for Using Cancellation Tokens
- Accept cancellation tokens as parameters: Design your asynchronous methods to accept a
CancellationToken
parameter, preferably as the last parameter with a default value.
public Task DoWorkAsync(string input, CancellationToken cancellationToken = default)
{
// Implementation
}
- Propagate cancellation tokens: Pass the token to other async methods you call.
public async Task ParentMethodAsync(CancellationToken cancellationToken)
{
// Pass the token to child methods
await ChildMethod1Async(cancellationToken);
await ChildMethod2Async(cancellationToken);
}
-
Check for cancellation at appropriate points: Check for cancellation at logical points in your code.
-
Clean up resources: When cancellation is requested, make sure to clean up any resources you've allocated.
-
Don't ignore cancellation exceptions: Handle
OperationCanceledException
appropriately. -
Dispose of cancellation token sources: Use the
using
statement or callDispose()
when you're done with aCancellationTokenSource
.
Common Pitfalls
- Not disposing
CancellationTokenSource
: This can lead to memory leaks. - Not handling
OperationCanceledException
: This can cause unhandled exceptions in your application. - Checking for cancellation too infrequently: This can make your application unresponsive to cancellation requests.
- Checking for cancellation too frequently: This can impact performance.
Summary
Cancellation tokens provide a standardized mechanism for canceling asynchronous operations in .NET. They enable you to build more responsive applications that can handle cancellations gracefully.
Key points to remember:
- Use
CancellationTokenSource
to create and signal cancellation - Pass
CancellationToken
to methods that support cancellation - Check for cancellation at appropriate points in your code
- Handle
OperationCanceledException
where appropriate - Clean up resources when cancellation occurs
- Always dispose of
CancellationTokenSource
instances
Exercises
-
Create a console application that downloads multiple files concurrently and allows the user to cancel the downloads by pressing a key.
-
Implement a search function that queries multiple APIs and can be canceled if it takes too long.
-
Modify an existing async method in your codebase to accept a cancellation token and properly handle cancellation.
Additional Resources
- .NET Documentation: Cancellation in Managed Threads
- Task Cancellation
- Implementing the Task-based Asynchronous Pattern
Happy coding!
If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)