1. CancellationTokenSource 基础概念解析
在.NET开发中,CancellationTokenSource是一个非常重要的并发控制类,它提供了取消异步操作的机制。这个类位于System.Threading命名空间,是.NET 4.0引入的Task Parallel Library(TPL)的核心组件之一。
CancellationTokenSource的核心作用是生成和管理CancellationToken对象。当我们需要取消一个或多个异步操作时,可以通过CancellationTokenSource发出取消信号,所有关联的CancellationToken都会接收到这个信号。
重要提示:CancellationTokenSource实现了IDisposable接口,使用完毕后应当调用Dispose方法释放资源,特别是在频繁创建和销毁的场景下。
1.1 基本使用模式
典型的CancellationTokenSource使用模式如下:
csharp复制var cts = new CancellationTokenSource();
var token = cts.Token;
// 启动一个可以取消的任务
Task.Run(() =>
{
while (!token.IsCancellationRequested)
{
// 执行工作
}
token.ThrowIfCancellationRequested();
}, token);
// 在需要取消的时候
cts.Cancel();
这种模式有几个关键点需要注意:
- 任务需要定期检查IsCancellationRequested属性
- 任务结束后应调用ThrowIfCancellationRequested()抛出OperationCanceledException
- 取消操作是协作式的,任务必须主动响应取消请求
1.2 核心属性与方法
CancellationTokenSource提供了几个关键成员:
- Token:获取与此源关联的CancellationToken
- Cancel():发出取消信号
- CancelAfter():延迟一段时间后自动取消
- Dispose():释放资源
CancellationToken本身也提供了一些有用的成员:
- IsCancellationRequested:是否已请求取消
- ThrowIfCancellationRequested():如果已请求取消则抛出异常
- Register():注册取消回调
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 高级应用场景
2.1 组合取消令牌
在实际开发中,我们经常需要组合多个取消源。例如,一个操作可能同时受用户取消和超时取消的影响:
csharp复制var userCts = new CancellationTokenSource();
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
// 创建一个组合取消源
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
userCts.Token,
timeoutCts.Token
);
try
{
await LongRunningOperationAsync(linkedCts.Token);
}
catch (OperationCanceledException)
{
// 判断是哪种取消
if (timeoutCts.IsCancellationRequested)
{
Console.WriteLine("操作超时");
}
else if (userCts.IsCancellationRequested)
{
Console.WriteLine("用户取消了操作");
}
}
2.2 超时控制
CancellationTokenSource内置支持超时控制,有两种使用方式:
- 构造函数指定超时时间:
csharp复制var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
- 使用CancelAfter方法:
csharp复制var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(30));
实践经验:对于Web请求、数据库查询等I/O操作,强烈建议总是设置合理的超时时间,避免因服务端问题导致客户端长时间等待。
2.3 与async/await配合使用
在异步编程中,CancellationTokenSource尤为重要:
csharp复制public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
await Task.Delay(1000, cancellationToken); // 可取消的延迟
using var httpClient = new HttpClient();
var response = await httpClient.GetAsync("https://api.example.com/data", cancellationToken);
// 处理响应...
}
3. 性能优化与最佳实践
3.1 资源管理
CancellationTokenSource使用了非托管资源,不当管理可能导致内存泄漏:
csharp复制// 不好的做法 - 可能泄漏资源
public CancellationToken GetToken()
{
var cts = new CancellationTokenSource();
return cts.Token; // 返回Token但无法访问CTS
}
// 好的做法
public (CancellationToken, CancellationTokenSource) GetToken()
{
var cts = new CancellationTokenSource();
return (cts.Token, cts);
}
3.2 注册回调的注意事项
使用Register方法注册取消回调时需要注意:
- 回调执行线程不确定,不要假设会在特定线程执行
- 回调应该快速完成,避免长时间运行
- 记得在适当的时候取消注册
csharp复制var cts = new CancellationTokenSource();
var registration = cts.Token.Register(() =>
{
// 清理工作
});
// 当不再需要时
registration.Dispose();
3.3 避免常见陷阱
-
Token传播:应该将CancellationToken作为参数传递给所有相关方法,而不是传递CancellationTokenSource。
-
多次取消:多次调用Cancel()是安全的,但只有第一次调用会实际触发取消。
-
竞争条件:在检查IsCancellationRequested和实际执行操作之间,取消状态可能已经改变。
4. 实际应用案例
4.1 Web API中的请求取消
在ASP.NET Core中,控制器方法自动接收一个CancellationToken参数,当客户端断开连接时,这个token会被取消:
csharp复制[HttpGet]
public async Task<IActionResult> GetData(CancellationToken cancellationToken)
{
try
{
var data = await _dataService.FetchDataAsync(cancellationToken);
return Ok(data);
}
catch (OperationCanceledException)
{
return StatusCode(499); // 客户端关闭请求
}
}
4.2 并行处理中的取消
使用Parallel.ForEach时也可以利用CancellationToken:
csharp复制var cts = new CancellationTokenSource();
var options = new ParallelOptions
{
CancellationToken = cts.Token,
MaxDegreeOfParallelism = Environment.ProcessorCount
};
try
{
Parallel.ForEach(items, options, item =>
{
ProcessItem(item);
options.CancellationToken.ThrowIfCancellationRequested();
});
}
catch (OperationCanceledException)
{
Console.WriteLine("处理被取消");
}
4.3 后台服务中的优雅关闭
在实现后台服务时,CancellationTokenSource可以帮助实现优雅关闭:
csharp复制public class BackgroundWorker : IHostedService
{
private CancellationTokenSource _cts;
public Task StartAsync(CancellationToken cancellationToken)
{
_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_ = Task.Run(() => DoWork(_cts.Token));
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
_cts?.Cancel();
return Task.CompletedTask;
}
private async Task DoWork(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
// 执行工作
await Task.Delay(1000, token);
}
catch (OperationCanceledException)
{
// 清理资源
break;
}
}
}
}
5. 深入理解实现原理
5.1 取消机制的内部实现
CancellationTokenSource的核心是一个ManualResetEvent和一个原子标志位。当调用Cancel()时:
- 原子标志位被设置为true
- 注册的回调被调用
- ManualResetEvent被设置
这种设计使得取消操作非常高效,且线程安全。
5.2 内存模型与线程安全
CancellationTokenSource的所有公共成员都是线程安全的:
- Cancel()方法使用内存屏障确保状态变化对所有线程可见
- IsCancellationRequested使用volatile读取
- 回调注册和调用使用线程安全的数据结构
5.3 与旧版.NET的兼容性
对于需要支持旧版.NET的项目,可以使用以下模式:
csharp复制#if NET40
var cts = new CancellationTokenSource();
#else
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
#endif
6. 常见问题排查
6.1 取消不生效的可能原因
- 任务没有定期检查IsCancellationRequested
- 没有传递CancellationToken到异步方法
- 使用了不支持取消的API
- 回调注册后没有正确保持引用
6.2 性能问题诊断
如果遇到性能问题,可以检查:
- 是否注册了大量回调
- 回调执行时间是否过长
- 是否频繁创建和销毁CancellationTokenSource
6.3 调试技巧
在调试时,可以:
- 检查CancellationToken.CanBeCanceled属性
- 使用调试器查看CancellationTokenSource的状态
- 在回调中设置断点
7. 扩展应用与创新用法
7.1 实现超时包装器
可以创建一个通用的超时包装方法:
csharp复制public static async Task<T> WithTimeout<T>(this Task<T> task, TimeSpan timeout)
{
var cts = new CancellationTokenSource(timeout);
var timeoutTask = Task.Delay(timeout, cts.Token);
var completedTask = await Task.WhenAny(task, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException();
}
cts.Cancel(); // 取消Delay任务
return await task;
}
7.2 构建可取消的LINQ查询
通过扩展方法实现可取消的LINQ操作:
csharp复制public static IEnumerable<T> AsCancelable<T>(
this IEnumerable<T> source,
CancellationToken token)
{
foreach (var item in source)
{
token.ThrowIfCancellationRequested();
yield return item;
}
}
7.3 实现进度报告与取消的组合
结合IProgress和CancellationToken:
csharp复制public async Task ProcessItemsAsync(
IEnumerable<Item> items,
IProgress<int> progress,
CancellationToken cancellationToken)
{
int total = items.Count();
int processed = 0;
foreach (var item in items)
{
cancellationToken.ThrowIfCancellationRequested();
await ProcessItemAsync(item, cancellationToken);
processed++;
progress.Report(processed * 100 / total);
}
}
在实际项目中,CancellationTokenSource的正确使用可以显著提高应用程序的响应性和健壮性。掌握它的各种用法和最佳实践,是成为一名优秀.NET开发者的重要一步。
