15 minlesson

Cancellation Patterns

Cancellation Patterns

Different scenarios require different cancellation strategies. This lesson covers common patterns for checking and responding to cancellation requests.

Pattern 1: Periodic Check in Loops

The most common pattern - check regularly during iteration:

csharp
1async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken ct)
2{
3 foreach (var item in items)
4 {
5 // Check at start of each iteration
6 ct.ThrowIfCancellationRequested();
7
8 await ProcessItemAsync(item);
9 }
10}

For CPU-bound loops, check less frequently to reduce overhead:

csharp
1void ComputeHeavy(CancellationToken ct)
2{
3 for (int i = 0; i < 10_000_000; i++)
4 {
5 // Check every 1000 iterations
6 if (i % 1000 == 0)
7 {
8 ct.ThrowIfCancellationRequested();
9 }
10
11 DoComputation(i);
12 }
13}

Pattern 2: Non-Throwing Check

When you need cleanup before exiting:

csharp
1async Task ProcessWithCleanupAsync(CancellationToken ct)
2{
3 Resource? resource = null;
4
5 try
6 {
7 resource = await AcquireResourceAsync();
8
9 while (HasMoreWork())
10 {
11 // Non-throwing check
12 if (ct.IsCancellationRequested)
13 {
14 Console.WriteLine("Cancellation requested, cleaning up...");
15 break; // Exit loop, but continue to finally
16 }
17
18 await DoWorkAsync();
19 }
20 }
21 finally
22 {
23 // Always cleanup
24 resource?.Dispose();
25 }
26
27 // Throw after cleanup if needed
28 ct.ThrowIfCancellationRequested();
29}

Pattern 3: Register Callback

Execute code when cancellation occurs:

csharp
1async Task OperationWithCallbackAsync(CancellationToken ct)
2{
3 // Register cleanup callback
4 using var registration = ct.Register(() =>
5 {
6 Console.WriteLine("Cancellation detected!");
7 // Perform immediate cleanup or notification
8 });
9
10 await LongRunningOperationAsync(ct);
11}

Multiple registrations are supported:

csharp
1ct.Register(() => CloseConnection());
2ct.Register(() => SaveState());
3ct.Register(() => NotifyUser());

Pattern 4: Pass to External APIs

Propagate the token to all async calls:

csharp
1async Task<Data> FetchAndProcessAsync(string url, CancellationToken ct)
2{
3 // Pass to HTTP call
4 var response = await httpClient.GetAsync(url, ct);
5
6 // Pass to stream read
7 var content = await response.Content.ReadAsStringAsync(ct);
8
9 // Pass to JSON deserialization
10 return JsonSerializer.Deserialize<Data>(content);
11}

Pattern 5: Combining Check with Async Wait

Some APIs don't support cancellation directly:

csharp
1async Task WaitForFileAsync(string path, CancellationToken ct)
2{
3 while (!File.Exists(path))
4 {
5 ct.ThrowIfCancellationRequested();
6 await Task.Delay(100, ct); // Delay IS cancelable
7 }
8}

Pattern 6: Graceful Shutdown

Complete current work unit before exiting:

csharp
1async Task ProcessBatchesAsync(CancellationToken ct)
2{
3 while (true)
4 {
5 // Check BETWEEN batches, not during
6 if (ct.IsCancellationRequested)
7 {
8 Console.WriteLine("Finishing current batch before shutdown...");
9 // Current batch will complete, then exit
10 }
11
12 var batch = await GetNextBatchAsync();
13 if (batch == null) break;
14
15 // Process entire batch without interruption
16 await ProcessBatchAsync(batch);
17
18 // Now safe to honor cancellation
19 ct.ThrowIfCancellationRequested();
20 }
21}

Pattern 7: Cancellation with State Save

Save progress before canceling:

csharp
1async Task ProcessWithCheckpointAsync(CancellationToken ct)
2{
3 int lastProcessed = await LoadCheckpointAsync();
4
5 try
6 {
7 for (int i = lastProcessed; i < TotalItems; i++)
8 {
9 if (ct.IsCancellationRequested)
10 {
11 // Save progress before exiting
12 await SaveCheckpointAsync(i);
13 ct.ThrowIfCancellationRequested();
14 }
15
16 await ProcessItemAsync(i);
17 }
18 }
19 catch (OperationCanceledException)
20 {
21 await SaveCheckpointAsync(lastProcessed);
22 throw;
23 }
24}

Pattern 8: WaitHandle for Non-Async Code

For code that can't use async:

csharp
1void BlockingOperation(CancellationToken ct)
2{
3 WaitHandle handle = ct.WaitHandle;
4
5 // Wait with cancellation support
6 int result = WaitHandle.WaitAny(new[]
7 {
8 someEvent,
9 handle // Returns if cancellation requested
10 });
11
12 if (result == 1) // Cancellation handle
13 {
14 throw new OperationCanceledException(ct);
15 }
16}

Anti-Patterns to Avoid

Don't ignore the token

csharp
1// BAD: Ignoring the token parameter
2async Task BadMethodAsync(CancellationToken ct)
3{
4 await Task.Delay(10000); // Should pass ct!
5}
6
7// GOOD:
8async Task GoodMethodAsync(CancellationToken ct)
9{
10 await Task.Delay(10000, ct);
11}

Don't check too frequently

csharp
1// BAD: Checking every iteration is expensive
2for (int i = 0; i < 1_000_000_000; i++)
3{
4 ct.ThrowIfCancellationRequested(); // Too often!
5 sum += i;
6}
7
8// GOOD: Check periodically
9for (int i = 0; i < 1_000_000_000; i++)
10{
11 if (i % 100_000 == 0)
12 ct.ThrowIfCancellationRequested();
13 sum += i;
14}

Don't catch and ignore

csharp
1// BAD: Swallowing cancellation
2try
3{
4 await DoWorkAsync(ct);
5}
6catch (OperationCanceledException)
7{
8 // Silently ignoring cancellation!
9}
10
11// GOOD: Re-throw or handle appropriately
12catch (OperationCanceledException)
13{
14 _logger.LogInformation("Operation canceled");
15 throw;
16}

Key Takeaways

  • Check cancellation at appropriate points (loop starts, between operations)
  • Use IsCancellationRequested when you need cleanup before throwing
  • Use ThrowIfCancellationRequested() for immediate cancellation
  • Register callbacks for immediate notification
  • Always propagate tokens to async APIs
  • Save state before honoring cancellation when needed
  • Don't check too frequently in tight loops