Linked Cancellation Tokens
Sometimes you need to cancel an operation when any of multiple conditions occur - user cancellation, timeout, or application shutdown. Linked tokens combine multiple cancellation sources.
The Problem
Consider an HTTP request that should cancel if:
- User clicks "Cancel"
- Request takes longer than 30 seconds
- Application is shutting down
Without linked tokens, you'd need complex logic:
csharp1// MESSY: Manual checking of multiple sources2async Task FetchDataAsync(CancellationToken userToken, CancellationToken appToken)3{4 var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));56 while (hasMoreData)7 {8 // Check all three!9 if (userToken.IsCancellationRequested ||10 appToken.IsCancellationRequested ||11 timeoutCts.Token.IsCancellationRequested)12 {13 throw new OperationCanceledException();14 }1516 await FetchChunkAsync(/* which token to pass?? */);17 }18}
CreateLinkedTokenSource
Combine multiple tokens into one:
csharp1async Task FetchDataAsync(CancellationToken userToken, CancellationToken appToken)2{3 using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));45 // Create linked token that cancels if ANY source cancels6 using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(7 userToken,8 appToken,9 timeoutCts.Token10 );1112 CancellationToken linkedToken = linkedCts.Token;1314 // Now just check/pass one token15 await FetchChunkAsync(linkedToken);16}
User Cancellation + Timeout
The most common use case:
csharp1public async Task<string> FetchWithTimeoutAsync(2 string url,3 TimeSpan timeout,4 CancellationToken userCancellation = default)5{6 using var timeoutCts = new CancellationTokenSource(timeout);7 using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(8 userCancellation,9 timeoutCts.Token);1011 try12 {13 return await httpClient.GetStringAsync(url, linkedCts.Token);14 }15 catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)16 {17 throw new TimeoutException($"Request timed out after {timeout}");18 }19 // User cancellation passes through as OperationCanceledException20}
Identifying Which Token Canceled
When cancellation occurs, you might need to know which source triggered it:
csharp1async Task ProcessAsync(CancellationToken userToken)2{3 using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(5));4 using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(5 userToken,6 timeoutCts.Token);78 try9 {10 await DoWorkAsync(linkedCts.Token);11 }12 catch (OperationCanceledException)13 {14 if (timeoutCts.IsCancellationRequested)15 {16 Console.WriteLine("Operation timed out");17 }18 else if (userToken.IsCancellationRequested)19 {20 Console.WriteLine("User canceled the operation");21 }22 else23 {24 Console.WriteLine("Unknown cancellation source");25 }2627 throw;28 }29}
Application Shutdown Pattern
Create a global shutdown token:
csharp1public class AppLifetime2{3 private readonly CancellationTokenSource _shutdownCts = new();45 public CancellationToken ShutdownToken => _shutdownCts.Token;67 public void SignalShutdown()8 {9 _shutdownCts.Cancel();10 }11}1213// In services14public class DataService15{16 private readonly AppLifetime _lifetime;1718 public async Task ProcessAsync(CancellationToken requestToken)19 {20 // Cancel on request cancellation OR app shutdown21 using var linked = CancellationTokenSource.CreateLinkedTokenSource(22 requestToken,23 _lifetime.ShutdownToken);2425 await LongRunningWorkAsync(linked.Token);26 }27}
Nested Operations
Each layer can add its own timeout:
csharp1// Outer operation: 60 second total timeout2public async Task OuterOperationAsync(CancellationToken ct)3{4 using var outerTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(60));5 using var linkedOuter = CancellationTokenSource.CreateLinkedTokenSource(6 ct, outerTimeout.Token);78 await InnerOperationAsync(linkedOuter.Token);9 await AnotherOperationAsync(linkedOuter.Token);10}1112// Inner operation: 10 second per-call timeout13private async Task InnerOperationAsync(CancellationToken ct)14{15 using var innerTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(10));16 using var linkedInner = CancellationTokenSource.CreateLinkedTokenSource(17 ct, innerTimeout.Token);1819 await httpClient.GetAsync(url, linkedInner.Token);20}
The inner operation cancels if:
- User cancels (original ct)
- Outer timeout (60s) expires
- Inner timeout (10s) expires
Dynamic Linking
Add sources after creation:
csharp1// Note: CreateLinkedTokenSource doesn't support adding more tokens later2// Create a new linked source if you need to add more34var sources = new List<CancellationToken> { userToken };56if (useTimeout)7{8 var timeoutCts = new CancellationTokenSource(timeout);9 sources.Add(timeoutCts.Token);10}1112if (appShutdownToken != default)13{14 sources.Add(appShutdownToken);15}1617using var linked = CancellationTokenSource.CreateLinkedTokenSource(18 sources.ToArray());
Memory and Disposal
Always dispose linked token sources!
csharp1// BAD: Memory leak2var linked = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);3await DoWorkAsync(linked.Token);4// linked is never disposed!56// GOOD: Using statement7using var linked = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);8await DoWorkAsync(linked.Token);
Common Pattern: Request with Timeout
csharp1public static class CancellationExtensions2{3 public static CancellationTokenSource WithTimeout(4 this CancellationToken token,5 TimeSpan timeout)6 {7 var timeoutCts = new CancellationTokenSource(timeout);8 var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(9 token,10 timeoutCts.Token);1112 return linkedCts;13 }14}1516// Usage17public async Task ProcessAsync(CancellationToken ct)18{19 using var cts = ct.WithTimeout(TimeSpan.FromSeconds(30));20 await DoWorkAsync(cts.Token);21}
Key Takeaways
CreateLinkedTokenSourcecombines multiple cancellation sources- The linked token cancels when any source cancels
- Check individual tokens to determine which source triggered cancellation
- Always dispose linked token sources
- Common pattern: user cancellation + timeout
- Useful for layered timeouts in nested operations
- Great for combining request cancellation with app shutdown