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:
csharp1async Task ProcessItemsAsync(IEnumerable<Item> items, CancellationToken ct)2{3 foreach (var item in items)4 {5 // Check at start of each iteration6 ct.ThrowIfCancellationRequested();78 await ProcessItemAsync(item);9 }10}
For CPU-bound loops, check less frequently to reduce overhead:
csharp1void ComputeHeavy(CancellationToken ct)2{3 for (int i = 0; i < 10_000_000; i++)4 {5 // Check every 1000 iterations6 if (i % 1000 == 0)7 {8 ct.ThrowIfCancellationRequested();9 }1011 DoComputation(i);12 }13}
Pattern 2: Non-Throwing Check
When you need cleanup before exiting:
csharp1async Task ProcessWithCleanupAsync(CancellationToken ct)2{3 Resource? resource = null;45 try6 {7 resource = await AcquireResourceAsync();89 while (HasMoreWork())10 {11 // Non-throwing check12 if (ct.IsCancellationRequested)13 {14 Console.WriteLine("Cancellation requested, cleaning up...");15 break; // Exit loop, but continue to finally16 }1718 await DoWorkAsync();19 }20 }21 finally22 {23 // Always cleanup24 resource?.Dispose();25 }2627 // Throw after cleanup if needed28 ct.ThrowIfCancellationRequested();29}
Pattern 3: Register Callback
Execute code when cancellation occurs:
csharp1async Task OperationWithCallbackAsync(CancellationToken ct)2{3 // Register cleanup callback4 using var registration = ct.Register(() =>5 {6 Console.WriteLine("Cancellation detected!");7 // Perform immediate cleanup or notification8 });910 await LongRunningOperationAsync(ct);11}
Multiple registrations are supported:
csharp1ct.Register(() => CloseConnection());2ct.Register(() => SaveState());3ct.Register(() => NotifyUser());
Pattern 4: Pass to External APIs
Propagate the token to all async calls:
csharp1async Task<Data> FetchAndProcessAsync(string url, CancellationToken ct)2{3 // Pass to HTTP call4 var response = await httpClient.GetAsync(url, ct);56 // Pass to stream read7 var content = await response.Content.ReadAsStringAsync(ct);89 // Pass to JSON deserialization10 return JsonSerializer.Deserialize<Data>(content);11}
Pattern 5: Combining Check with Async Wait
Some APIs don't support cancellation directly:
csharp1async Task WaitForFileAsync(string path, CancellationToken ct)2{3 while (!File.Exists(path))4 {5 ct.ThrowIfCancellationRequested();6 await Task.Delay(100, ct); // Delay IS cancelable7 }8}
Pattern 6: Graceful Shutdown
Complete current work unit before exiting:
csharp1async Task ProcessBatchesAsync(CancellationToken ct)2{3 while (true)4 {5 // Check BETWEEN batches, not during6 if (ct.IsCancellationRequested)7 {8 Console.WriteLine("Finishing current batch before shutdown...");9 // Current batch will complete, then exit10 }1112 var batch = await GetNextBatchAsync();13 if (batch == null) break;1415 // Process entire batch without interruption16 await ProcessBatchAsync(batch);1718 // Now safe to honor cancellation19 ct.ThrowIfCancellationRequested();20 }21}
Pattern 7: Cancellation with State Save
Save progress before canceling:
csharp1async Task ProcessWithCheckpointAsync(CancellationToken ct)2{3 int lastProcessed = await LoadCheckpointAsync();45 try6 {7 for (int i = lastProcessed; i < TotalItems; i++)8 {9 if (ct.IsCancellationRequested)10 {11 // Save progress before exiting12 await SaveCheckpointAsync(i);13 ct.ThrowIfCancellationRequested();14 }1516 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:
csharp1void BlockingOperation(CancellationToken ct)2{3 WaitHandle handle = ct.WaitHandle;45 // Wait with cancellation support6 int result = WaitHandle.WaitAny(new[]7 {8 someEvent,9 handle // Returns if cancellation requested10 });1112 if (result == 1) // Cancellation handle13 {14 throw new OperationCanceledException(ct);15 }16}
Anti-Patterns to Avoid
Don't ignore the token
csharp1// BAD: Ignoring the token parameter2async Task BadMethodAsync(CancellationToken ct)3{4 await Task.Delay(10000); // Should pass ct!5}67// GOOD:8async Task GoodMethodAsync(CancellationToken ct)9{10 await Task.Delay(10000, ct);11}
Don't check too frequently
csharp1// BAD: Checking every iteration is expensive2for (int i = 0; i < 1_000_000_000; i++)3{4 ct.ThrowIfCancellationRequested(); // Too often!5 sum += i;6}78// GOOD: Check periodically9for (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
csharp1// BAD: Swallowing cancellation2try3{4 await DoWorkAsync(ct);5}6catch (OperationCanceledException)7{8 // Silently ignoring cancellation!9}1011// GOOD: Re-throw or handle appropriately12catch (OperationCanceledException)13{14 _logger.LogInformation("Operation canceled");15 throw;16}
Key Takeaways
- Check cancellation at appropriate points (loop starts, between operations)
- Use
IsCancellationRequestedwhen 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