异步编程使得应用程序在进行 I/O 操作、网络请求或长时间运行的计算任务时保持响应性。对于这些长时间或可中断的任务,仅仅启动异步并不足够;我们还需要一种机制,在用户取消或资源不再需要时让任务及时结束,释放资源。C# 的 CancellationToken 就是为此设计的一等工具。以下是关于它的原理、实战用法与最佳实践的深入解析。
CancellationToken 与 CancellationTokenSource 的基本机制
CancellationTokenSource(CTS) 是外部控制取消信号的实体。它提供一个 Token 属性,代表将要传递出去以供观察和响应取消。通过调用 cts.Cancel() 或 cts.CancelAfter(...) 等机制发出取消请求。
CancellationToken 是一个可观察取消状态的结构体。任务或方法接收这个 token 后,可以定期检查它的状态(比如 IsCancellationRequested),或者调用 ThrowIfCancellationRequested() 来主动触发取消操作。
注册回调:通过 token.Register(...) 可以注册回调函数,一旦取消被触发,该回调会立即执行(同步触发),用于执行某些清理逻辑或协作取消已有的资源或第三方 API。
实战示例:如何在真实场景中用 CancellationToken
下面是一个较完整的实战流程示例,演示如何在控制台应用或服务中使用 CancellationToken 来支持取消操作。
using System;
using System.Threading;
using System.Threading.Tasks;
class Worker
{
public async Task RunAsync(CancellationToken cancellationToken)
{
try
{
// 模拟工作分批次进行
for (int i = 0; i < 20; i++)
{
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"Working batch #{i+1}");
// 模拟异步 I/O 或耗时操作
await Task.Delay(1000, cancellationToken);
}
Console.WriteLine("Work completed successfully.");
}
catch (OperationCanceledException)
{
Console.WriteLine("Work was canceled.");
// 在此执行额外清理逻辑(如果有)
}
finally
{
// 清理资源等
Console.WriteLine("Cleanup after job (if needed).");
}
}
}
class Program
{
static async Task Main(string[] args)
{
using (CancellationTokenSource cts = new CancellationTokenSource())
{
var worker = new Worker();
Task workTask = worker.RunAsync(cts.Token);
// 模拟用户输入取消操作
Console.WriteLine("Press 'c' to cancel...");
if (Console.ReadKey(true).KeyChar == 'c')
{
cts.Cancel();
}
await workTask;
}
}
}
在这个例子中,我们演示了:
- 在长循环中定期检查取消状态
- 在 Task.Delay 等异步操作中传入 token,让底层等待也可被取消
- 捕获 OperationCanceledException 并执行清理
- 使用 finally 或 using 来清理资源
设计可取消 API 与模式
为了让应用程序整体结构健壮、可维护,需要在 API 设计和调用链中正确地传播取消信号。以下是一些模式与建议:
1. 将 CancellationToken 参数放在方法参数列表的最后
这是一个 .NET Code Quality 的建议规则(CA1068)。这样做可以使方法签名清晰,同时在可选参数的情况下更便于默认值处理。
2. 公共 API 可将 token 参数设为可选;内部逻辑方法强制传递
在暴露给外部调用者(例如 SDK 或 Web API 控制器)的方法中,可以给 CancellationToken cancellationToken = default,这样调用者可以选择不传。但在内部方法中接收 token 并传递给下层依赖是好习惯。
3. 尽早检查取消,但避免在关键不可恢复状态下取消
在任务流程中,如果已经产生了副作用(例如文件已写入、外部服务已调用等)且无法安全回滚,应该明确哪个阶段之后取消无效或只做补偿性操作。
4. 使用 CancellationTokenSource.CreateLinkedTokenSource 来组合多个取消信号
比如一个操作可能既受用户取消控制,也受超时控制,可以将这两个 token 链接起来,统一响应。
5. 及时释放与清理
CancellationTokenSource 是一个资源,如果创建后不用了,应当 Dispose。注册的回调也应当注意是否需要去掉注册,以避免内存泄漏。
6. 捕获取消异常并重新抛出或处理
在方法中捕获 OperationCanceledException(也包括 TaskCanceledException),做必要清理后,如果需要向上层告知取消状态,应重新抛出;如果方法的调用方期望成功返回,也可以返回标识状态。
常见问题与陷阱
在实践中,下面这些是很多人容易踩坑或忽略的地方:
忽略 token,导致任务永不停止
一个常见错误是启动异步任务但内部并未检查 IsCancellationRequested,或没有在支持取消的异步等待(比如 Task.Delay(token), HttpClient.SendAsync(..., token))中传入 token。
在同步阻塞调用中使用 token 无效
如果你在任务内部调用了阻塞型同步操作(例如 Thread.Sleep(...) 而不是 Task.Delay(...) 带 token),那么取消请求在这些同步调用中不会被立即响应。
副作用后取消导致不一致状态
如前所述,如果一个操作已经对外产生某些变更,而随后被取消,会导致系统状态可能不一致,需要设计补偿 (compensating) 或回滚机制。
滥用 token 参数导致方法签名混乱
Token 参数放错位置或者少传、多传都可能导致 API 难以使用和维护。遵循规则使代码一致性好。
高级用法与性能调优
为了在复杂系统或高并发环境中更好利用 CancellationToken,可以考虑以下进阶做法:
超时控制:使用 CancellationTokenSource.CancelAfter(...) 或与超时 timer 结合来实现操作超时。
批量并发任务的取消控制:当有多个子任务并行工作,可将相同 token 传递给多个任务,在某一个条件触发时统一取消全部。
检查 token.CanBeCanceled:如果 token 是 CancellationToken.None 或者某些情况下根本不会被取消,可以省去一些频繁检查的开销。
避免注册过多回调:每个 Register(...) 都会带来额外的资源占用与开销。回调函数应尽可能简单且快速执行。
使用 async 流(IAsyncEnumerable)与取消:在遍历异步流时,把 token 传给 foreach await 中的底层等待,使得对流的消费也能被取消。
在 Web API 或服务中的真实应用场景
控制器操作(Controller Actions)
在 ASP.NET Core 的 Web API 控制器方法中,可以接受 CancellationToken(通常由框架注入,代表 HTTP 请求取消,如客户端断开连接或请求超时),并将其传给下游异步方法。这能帮助节省服务器资源。
但如果控制器操作涉及数据库写入、跨服务调用等一系列副作用,应慎重选择哪些阶段会响应取消,哪些阶段要完成(或补偿)。
定时任务或后台服务
对于持续运行或周期性任务(例如监控、日志收集、消息队列消费等),设计时要考虑任务可能被取消(例如应用关闭、部署更新等),需在任务逻辑中监听 token,并在接收到取消时优雅关闭。
总结
CancellationToken 是 C# 异步编程中不可或缺的机制,它提供了一个协作式取消模型,让任务响应外部取消请求、释放资源并保持响应性。
掌握它的基础用法只是起点;设计可取消 API、处理副作用、组合多个取消来源、性能优化等更深入的实践才是让异步系统健壮且高效的关键。通过遵循本文中的最佳实践与实战技巧,可以在你的项目中更安全、更可维护、更高效地使用 CancellationToken。