在.NET开发中,取消令牌(CancellationToken)是一个看似简单但实际使用中充满陷阱的机制。作为一名长期奋战在.NET生产环境的老兵,我见过太多因为不当使用取消令牌导致的性能问题、内存泄漏和系统不稳定。这篇文章将分享我在实际项目中积累的5个关键经验教训,帮助开发者避开这些深坑。
CancellationTokenSource(CTS)是.NET中管理取消操作的核心类,它实际上持有三种关键资源:
这些资源如果不及时释放,会一直占用内存直到垃圾回收器(GC)运行。但在高并发场景下,GC可能不会及时触发,导致系统资源被持续占用。
去年我们遇到一个线上服务内存持续增长的问题。经过内存转储分析,发现是测试代码中创建的CTS没有被释放,这些CTS在长时间运行的集成测试中累积,最终导致内存耗尽。
解决方案很简单但很关键:
csharp复制using var cts = new CancellationTokenSource();
await DoWorkAsync(cts.Token);
专业提示:即使在单元测试中也要养成释放CTS的习惯,因为测试运行器可能长时间保持进程运行。
CancellationToken是结构体,传递成本低,这容易让人产生"可以随意共享"的错觉。但实际上,共享令牌会在不相关的操作之间创建隐式依赖。
我曾调试过一个电商系统的问题:用户取消订单操作偶尔会导致后台报表生成失败。最终发现是因为两个不相关的服务共享了同一个CTS。
每个独立的操作应该有自己的CTS:
csharp复制using var cts1 = new CancellationTokenSource();
await ProcessOrderAsync(cts1.Token);
using var cts2 = new CancellationTokenSource();
await GenerateReportAsync(cts2.Token);
如果确实需要协调多个操作的取消,应该使用链接令牌:
csharp复制using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userCancelToken,
systemTimeoutToken);
await ExecuteTransactionAsync(linkedCts.Token);
当一个方法接受CancellationToken参数时,它实际上与调用方建立了一个契约:
根据场景不同,我们可以选择两种取消响应方式:
传播取消(抛出异常)
csharp复制foreach (var item in items)
{
token.ThrowIfCancellationRequested();
Process(item);
}
适合场景:
静默取消(直接返回)
csharp复制foreach (var item in items)
{
if (token.IsCancellationRequested)
{
LogCancellation(itemsProcessed);
return;
}
Process(item);
}
适合场景:
关于取消的日志记录有几个要点:
理解CancellationTokenSource的内部实现有助于写出高性能代码:
在高吞吐服务中,频繁创建/销毁CTS会成为性能瓶颈。我们通过以下方式优化:
基于多年实战经验,我总结了以下五条黄金法则:
取消功能必须被测试,这是很多团队忽视的一点。基本测试模式:
csharp复制[Fact]
public async Task Service_Should_Respond_To_Cancellation()
{
using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100));
await Assert.ThrowsAsync<OperationCanceledException>(() =>
myService.LongRunningOperationAsync(cts.Token));
}
更全面的测试应该包括:
在最近的一个分布式系统中,我们实现了以下取消策略:
这套策略帮助我们实现了:
取消机制是.NET并发编程中的重要工具,正确使用可以提升系统健壮性,错误使用则会导致各种隐蔽问题。希望这些实战经验能帮助你在项目中更好地驾驭这一强大功能。