当需要将多个 CancellationToken 合并为“任一取消即触发”的联合令牌时,应使用 CancellationTokenSource.CreateLinkedTokenSource,典型场景包括用户主动取消、超时终止和外部健康检查三类信号的协同响应。
CancellationTokenSource.CreateLinkedTokenSource
当你需要把多个 CancellationToken 合并成一个“只要任一取消就触发”的联合令牌时,就该用它。典型场景是:一个长期运行的任务既要响应用户主动取消(比如 UI 点击“停止”),又要响应超时(比如 30 秒后自动终止),这两个信号互不依赖,但任务只需响应其中任意一个即可退出。
CreateLinkedTokenSource 的参数和常见误用它接受一个或多个 CancellationToken 参数,返回一个新的 CancellationTokenSource,其 Token 在任一源被取消时立即变为已取消状态。注意:它不接受 CancellationTokenSource,只接受 CancellationToken;传入已取消的令牌会立刻让新源也进入取消状态;传入 default(CancellationToken) 是安全的(会被忽略)。
CancellationTokenSource.CreateLinkedTokenSource(userToken, timeoutToken)
CancellationTokenSource.CreateLinkedTokenSource(userCts.Token, null) —— null 会抛 ArgumentNullException
Token 已经被取消,新创建的 CancellationTokenSource 会立即处于 IsCancellationRequested
== true 状态,且无法再调用 Cancel()(会抛异常)实际项目中常需叠加三类信号。关键点在于:每个信号源应独立管理生命周期,避免因某一个提前取消而意外影响其他源;链接操作应在真正启动任务前完成。
var userCts = new CancellationTokenSource();
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var healthCts = new CancellationTokenSource(); // 可由后台线程监听服务状态后调用 Cancel()
// 链接三个令牌
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userCts.Token,
timeoutCts.Token,
healthCts.Token);
try
{
await DoWorkAsync(linkedCts.Token); // 任一信号触发,DoWorkAsync 内部应检查 Token 并及时退出
}
catch (OperationCanceledException) when (linkedCts.IsCancellationRequested)
{
// 正常取消路径,无需重抛
}
Task.WhenAny 替代?因为语义不同:WhenAny 是等待任意一个 Task 完成,适用于并发任务协调;而 CreateLinkedTokenSource 是对取消信号做逻辑“或”合并,作用于同一个任务的协作式取消流程。前者解决“谁先做完”,后者解决“谁先喊停”。混淆会导致:
DoWorkAsync 内部通过 token.ThrowIfCancellationRequested() 响应取消真正难处理的是跨线程传递和持有 CancellationTokenSource 的生命周期管理——比如 healthCts 由另一个模块控制,必须确保它不会被过早 GC 或重复 Cancel。这点容易被忽略,但直接影响可靠性。