C# Cancellation
In asynchronous programming, operations often take time to complete. Sometimes, you may want to cancel these operations before they finish - perhaps because the user clicked a "Cancel" button, a timeout occurred, or the application is shutting down. C# provides a clean and standardized way to handle cancellation through the CancellationToken
pattern.
Introduction to Cancellation
Cancellation in C# is a cooperative mechanism, which means:
- The code that wants to cancel an operation creates and manages a cancellation request
- The code that performs the operation periodically checks if cancellation was requested and responds appropriately
This approach ensures that operations can be canceled safely without causing resource leaks or leaving the application in an inconsistent state.
The Cancellation Mechanism
The cancellation system in C# revolves around two main classes:
CancellationTokenSource
: Creates and manages cancellation tokensCancellationToken
: Represents a cancellation request and is passed to cancellable operations
Basic Cancellation Example
Let's start with a simple example:
using System;
using System.Threading;
using System.Threading.Tasks;
public class Program
{
public static async Task Main()
{
// Create a cancellation token source
using var cts = new CancellationTokenSource();
// Start a task that will use the token
Task countingTask = CountToHighNumberAsync(cts.Token);
// Wait for 3 seconds and then cancel the operation
await Task.Delay(3000);
Console.WriteLine("Cancelling the operation...");
cts.Cancel();
try
{
await countingTask;
Console.WriteLine("Task completed successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Task was cancelled as expected.");
}
catch (Exception ex)
{
Console.WriteLine($"Task failed with exception: {ex.Message}");
}
}
static async Task CountToHighNumberAsync(CancellationToken token)
{
Console.WriteLine("Starting to count...");
for (int i = 1; i <= 1000000; i++)
{
// Check if cancellation was requested
if (token.IsCancellationRequested)
{
Console.WriteLine($"Cancellation requested at count: {i}");
token.ThrowIfCancellationRequested();
}
// Simulate work
if (i % 100000 == 0)
{
Console.WriteLine($"Counted to {i}");
await Task.Delay(500, token);
}
}
Console.WriteLine("Counting completed!");
}
}
Output:
Starting to count...
Counted to 100000
Counted to 200000
Counted to 300000
Cancelling the operation...
Cancellation requested at count: 345092
Task was cancelled as expected.
In this example:
- We create a
CancellationTokenSource
- We start a task that accepts the token
- After 3 seconds, we call
Cancel()
on the source - The task checks for cancellation and throws an
OperationCanceledException
- We catch the exception and handle the cancellation gracefully
Cancellation Best Practices
1. Add Cancellation Support to Your Methods
When creating methods that perform long-running operations, accept a CancellationToken
parameter:
public async Task<string> DownloadDataAsync(string url, CancellationToken token = default)
{
// Use the token for cancellation
using var httpClient = new HttpClient();
return await httpClient.GetStringAsync(url, token);
}
The default
value makes the parameter optional, allowing callers who don't need cancellation to skip providing a token.
2. Check for Cancellation Frequently
Check for cancellation regularly, especially in loops or before expensive operations:
for (int i = 0; i < largeCollection.Count; i++)
{
// Check before each iteration
token.ThrowIfCancellationRequested();
// Process item
await ProcessItemAsync(largeCollection[i]);
}
3. Pass the Token to Other Async Operations
Make sure to pass your cancellation token to other async methods you call:
public async Task ProcessFileAsync(string filePath, CancellationToken token)
{
// Pass the token to File.ReadAllTextAsync
string content = await File.ReadAllTextAsync(filePath, token);
// Pass the token to your own methods
await ProcessContentAsync(content, token);
}
Advanced Cancellation Techniques
Timeout-Based Cancellation
You can set a timeout for an operation using CancellationTokenSource.CancelAfter()
:
using var cts = new CancellationTokenSource();
// Automatically cancel after 5 seconds
cts.CancelAfter(TimeSpan.FromSeconds(5));
try
{
await LongRunningOperationAsync(cts.Token);
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation timed out after 5 seconds");
}
Linking Multiple Cancellation Sources
You can combine multiple cancellation sources:
// Create the first source for user cancellation
using var userCancellationSource = new CancellationTokenSource();
// Create the second source for timeout
using var timeoutCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
// Link the two sources
using var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
userCancellationSource.Token, timeoutCancellationSource.Token);
try
{
// Use the combined token that will be cancelled if either source is cancelled
await LongRunningOperationAsync(linkedSource.Token);
}
catch (OperationCanceledException)
{
if (userCancellationSource.IsCancellationRequested)
Console.WriteLine("Operation was cancelled by the user");
else if (timeoutCancellationSource.IsCancellationRequested)
Console.WriteLine("Operation timed out");
else
Console.WriteLine("Operation was cancelled");
}
Real-World Example: Cancellable Search Operation
Let's implement a more practical example of cancellation with a simulated search operation:
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
public class SearchService
{
public async Task<List<string>> SearchAsync(string query, CancellationToken cancellationToken)
{
Console.WriteLine($"Starting search for: '{query}'");
List<string> results = new List<string>();
// Simulate searching across multiple data sources
var datasources = new[] { "Web", "Database", "Local Cache", "API" };
foreach (var source in datasources)
{
// Check cancellation before starting each source
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"Searching in {source}...");
// Simulate the search taking different times for different sources
int delay = source == "Database" ? 3000 : 1000;
try
{
await Task.Delay(delay, cancellationToken);
}
catch (OperationCanceledException)
{
Console.WriteLine($"Search in {source} was cancelled");
throw;
}
// Add simulated results
results.Add($"{source} result for '{query}'");
Console.WriteLine($"Found result in {source}");
}
return results;
}
}
public class SearchApp
{
public static async Task RunSearchExample()
{
var searchService = new SearchService();
using var cts = new CancellationTokenSource();
// Start the search
Console.WriteLine("Starting search operation...");
Task<List<string>> searchTask = searchService.SearchAsync("C# async programming", cts.Token);
// Simulate user pressing cancel after 2 seconds
await Task.Delay(2000);
Console.WriteLine("User pressed cancel button");
cts.Cancel();
try
{
var results = await searchTask;
Console.WriteLine($"Search completed with {results.Count} results:");
foreach (var result in results)
{
Console.WriteLine($"- {result}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Search was cancelled by user");
}
}
}
Output:
Starting search operation...
Starting search for: 'C# async programming'
Searching in Web...
Found result in Web
Searching in Database...
User pressed cancel button
Search in Database was cancelled
Search was cancelled by user
In this example, the search operation checks for cancellation before searching each data source and properly handles cancellation during the delay operation.
Handling Cleanup on Cancellation
When an operation is canceled, you might need to clean up resources. You can do this using try/finally
:
public async Task ProcessWithCleanupAsync(CancellationToken token)
{
// Acquire some resource
var resource = await AcquireExpensiveResourceAsync();
try
{
// Use the resource in a cancellable operation
await UseResourceAsync(resource, token);
}
finally
{
// Clean up regardless of how the operation ended (success, error, or cancellation)
await resource.DisposeAsync();
}
}
Summary
Cancellation is an important aspect of creating responsive, user-friendly applications in C#. Here's what we've covered:
- The
CancellationToken
andCancellationTokenSource
classes provide a standardized way to handle cancellation - Cancellation is cooperative - both the caller and the method need to participate
- Methods should check for cancellation frequently and respond appropriately
- You can create timeout-based cancellations and link multiple cancellation sources
- Always ensure proper cleanup of resources when operations are canceled
By implementing cancellation properly in your asynchronous code, you'll create applications that are more responsive and give users more control over long-running operations.
Exercises
- Create a file download method that accepts a
CancellationToken
and properly handles cancellation. - Implement a search function with both a timeout and user cancellation using linked cancellation tokens.
- Modify the counting example to report progress using
IProgress<T>
while still supporting cancellation. - Create a worker service that performs periodic tasks and can be gracefully shut down using cancellation.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)