在 C# 中,异步任务取消机制是异步编程中处理任务中断的核心功能,广泛应用于需要响应用户操作、超时或外部条件终止任务的场景。CancellationTokenSource 和 CancellationToken 是实现异步任务取消的标准工具,基于协作式取消模型,允许任务优雅地停止执行。以下是对异步任务取消机制的详细解析,结合前述内容,重点优化异步生产者-消费者模型,提供适用场景、示例代码、测试用例,并深入探讨实现细节和最佳实践。
一、异步任务取消机制的原理
异步任务取消机制通过 CancellationTokenSource 和 CancellationToken 实现,允许开发者在异步任务中检测取消请求并采取相应行动。取消是协作式的,任务需主动检查取消状态或使用支持取消的异步 API。
核心组件
- 控制端,负责创建 CancellationToken 并触发取消信号。
- 提供方法如 Cancel(), CancelAfter(TimeSpan) 和构造函数超时设置。
- 状态不可逆:一旦取消,无法重置。
- 信号端,传递给任务,用于检查取消状态(IsCancellationRequested)或抛出异常(ThrowIfCancellationRequested)。
- 支持注册回调(Register)以执行清理逻辑。
- 任务需主动检查 CancellationToken 状态,决定是否停止。
- 支持取消的异步 API(如 Task.Delay, HttpClient.GetAsync)会抛出 OperationCanceledException 或其子类(如 TaskCanceledException)。
- 取消通常导致 OperationCanceledException,需在调用端捕获。
- 异步任务通过 await 传播取消异常。
工作流程
二、适用场景
异步任务取消机制适用于以下场景:
- 场景:用户在 UI 中点击“取消”按钮,终止异步操作(如数据加载、文件下载)。
- 示例:WPF 应用中取消后台查询。
- 场景:任务超过指定时间未完成,自动取消。
- 示例:网络请求 5 秒超时。
- 场景:多个并行异步任务,需统一取消。
- 示例:并行处理多个文件,取消后停止所有任务。
- 场景:异步任务队列中,取消生产或消费操作。
- 示例:消息处理系统因超时或错误取消任务。
- 场景:因资源限制或错误终止任务。
- 示例:数据库连接失败后取消查询。
三、优化异步生产者-消费者模型的取消机制
基于前述的异步生产者-消费者模型,我们进一步优化其取消机制,结合 CancellationTokenSource 和 BlockingCollection,实现高效、可靠的异步任务取消。以下示例支持用户取消和超时取消,并优化性能和鲁棒性。
示例代码:异步生产者-消费者模型带取消机制
场景:多个生产者异步生成任务,多个消费者异步处理任务,支持用户取消和 5 秒超时。
代码:
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class AsyncProducerConsumerWithCancellation
{
private static readonly BlockingCollection<int> _queue = new BlockingCollection<int>(boundedCapacity: 5);
private static int _taskIdCounter = 0;
static async Task Main()
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) // 5秒超时
{
try
{
// 启动生产者和消费者任务
Task[] producers = new Task[2];
Task[] consumers = new Task[3];
for (int i = 0; i < producers.Length; i++)
{
int id = i + 1;
producers[i] = Task.Run(() => ProducerAsync(id, cts.Token), cts.Token);
}
for (int i = 0; i < consumers.Length; i++)
{
int id = i + 1;
consumers[i] = Task.Run(() => ConsumerAsync($"消费者{id}", cts.Token), cts.Token);
}
// 模拟用户取消(可选,注释掉以测试超时)
// Task.Delay(3000, cts.Token).ContinueWith(_ => cts.Cancel(), cts.Token);
await Task.WhenAll(producers);
_queue.CompleteAdding(); // 生产完成,通知消费者
await Task.WhenAll(consumers);
Console.WriteLine("所有任务正常完成");
}
catch (OperationCanceledException)
{
Console.WriteLine("任务因取消(超时或用户)终止");
}
catch (Exception ex)
{
Console.WriteLine($"发生异常:{ex.Message}");
}
}
}
static async Task ProducerAsync(int producerId, CancellationToken token)
{
try
{
for (int i = 0; i < 5; i++)
{
token.ThrowIfCancellationRequested();
int taskId = Interlocked.Increment(ref _taskIdCounter);
_queue.Add(taskId, token); // 支持取消的添加
Console.WriteLine($"生产者{producerId} 生产任务:{taskId}");
await Task.Delay(500, token); // 模拟异步生产
}
}
catch (OperationCanceledException)
{
Console.WriteLine($"生产者{producerId} 被取消");
}
catch (Exception ex)
{
Console.WriteLine($"生产者{producerId} 异常:{ex.Message}");
}
}
static async Task ConsumerAsync(string name, CancellationToken token)
{
try
{
foreach (var taskId in _queue.GetConsumingEnumerable(token))
{
Console.WriteLine($"{name} 处理任务:{taskId}");
await Task.Delay(800, token); // 模拟异步处理
}
Console.WriteLine($"{name} 正常退出");
}
catch (OperationCanceledException)
{
Console.WriteLine($"{name} 被取消");
}
catch (Exception ex)
{
Console.WriteLine($"{name} 异常:{ex.Message}");
}
}
}
关键点:
- 取消支持:BlockingCollection.Add 和 GetConsumingEnumerable 使用 CancellationToken,自动响应取消。
- 超时设置:通过 new CancellationTokenSource(TimeSpan.FromSeconds(5)) 设置 5 秒超时。
- 用户取消:支持手动调用 cts.Cancel()(示例中注释掉,可启用测试)。
- 异步操作:使用 Task.Delay 模拟生产和消费的异步耗时,传递 CancellationToken。
- 线程安全:Interlocked.Increment 确保任务 ID 唯一。
- 异常处理:捕获 OperationCanceledException 和其他异常,确保鲁棒性。
- 资源清理:using 释放 CancellationTokenSource,CompleteAdding 优雅关闭队列。
运行结果(示例,5秒超时):
生产者1 生产任务:1
生产者2 生产任务:2
消费者1 处理任务:1
消费者2 处理任务:2
生产者1 生产任务:3
生产者2 生产任务:4
消费者3 处理任务:3
消费者1 处理任务:4
生产者1 生产任务:5
生产者2 生产任务:6
消费者2 处理任务:5
任务因取消(超时或用户)终止
生产者1 被取消
生产者2 被取消
消费者1 被取消
消费者2 被取消
消费者3 被取消
四、测试用例
以下是针对异步生产者-消费者模型的取消机制的测试用例,验证超时和用户取消功能。
测试用例 1:验证超时取消
目标:确保任务在 3 秒超时后正确取消。
测试代码:
using System;
using System.Threading.Tasks;
class CancellationTest
{
static async Task TestTimeoutCancellation()
{
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
{
try
{
await AsyncProducerConsumerWithCancellation.ProducerAsync(1, cts.Token);
Console.WriteLine("测试失败:生产者未触发超时");
}
catch (OperationCanceledException)
{
Console.WriteLine("测试通过:生产者因超时取消");
}
}
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)))
{
try
{
await AsyncProducerConsumerWithCancellation.ConsumerAsync("测试消费者", cts.Token);
Console.WriteLine("测试失败:消费者未触发超时");
}
catch (OperationCanceledException)
{
Console.WriteLine("测试通过:消费者因超时取消");
}
}
}
static async Task Main()
{
await TestTimeoutCancellation();
}
}
预期结果:
- 生产者和消费者运行约 3 秒后因超时取消,输出:测试通过:生产者因超时取消
测试通过:消费者因超时取消
测试用例 2:验证用户取消
目标:验证用户手动触发取消后,任务正确停止。
测试代码:
using System;
using System.Threading;
using System.Threading.Tasks;
class CancellationTest
{
static async Task TestUserCancellation()
{
using (var cts = new CancellationTokenSource())
{
var producerTask = Task.Run(() => AsyncProducerConsumerWithCancellation.ProducerAsync(1, cts.Token), cts.Token);
var consumerTask = Task.Run(() => AsyncProducerConsumerWithCancellation.ConsumerAsync("测试消费者", cts.Token), cts.Token);
await Task.Delay(2000); // 模拟用户 2 秒后取消
cts.Cancel();
try
{
await Task.WhenAll(producerTask, consumerTask);
Console.WriteLine("测试失败:未触发取消");
}
catch (OperationCanceledException)
{
Console.WriteLine("测试通过:任务因用户取消终止");
}
}
}
static async Task Main()
{
await TestUserCancellation();
}
}
预期结果:
- 任务运行约 2 秒后因用户取消终止,输出:测试通过:任务因用户取消终止
五、优化点与注意事项
优化点
- 内置支持 CancellationToken,简化异步队列管理。
- Add(token) 和 GetConsumingEnumerable(token) 自动响应取消。
- 支持 CancelAfter 和手动 Cancel,适应多种取消场景。
- 示例中通过构造函数设置超时,可动态调整。
- 使用 Task.Delay 等异步 API,传递 CancellationToken 确保取消支持。
- Task.Run 也传递 CancellationToken,确保任务调度可取消。
- BlockingCollection 内部优化锁机制,减少竞争。
- 批量处理可进一步减少队列操作开销(可扩展)。
- 捕获 OperationCanceledException 和其他异常。
- 使用 _queue.CompleteAdding() 优雅关闭队列。
注意事项
- 在循环或异步等待点调用 ThrowIfCancellationRequested:token.ThrowIfCancellationRequested();
- 或者检查 IsCancellationRequested:if (token.IsCancellationRequested) return;
- 确保异步方法(如 HttpClient.GetAsync, Task.Delay) 支持 CancellationToken。
- 非支持取消的 API 需手动检查:while (!token.IsCancellationRequested) { /* 非异步操作 */ }
- 使用 using 释放 CancellationTokenSource:using (var cts = new CancellationTokenSource()) { /* 使用 */ }
- 清理回调:var registration = token.Register(() => Console.WriteLine("清理"));
registration.Dispose();
- 取消后的 CancellationTokenSource 不可重用,需创建新实例:cts = new CancellationTokenSource();
- 捕获 TaskCanceledException 或 OperationCanceledException:try { await task; } catch (OperationCanceledException) { /* 处理 */ }
六、总结与最佳实践
核心价值
- 灵活取消:支持超时、用户取消和条件取消,适应多种异步场景。
- 异步集成:与 async/await 和 TPL 无缝配合,适合现代 .NET 应用。
- 高鲁棒性:通过异常处理和资源清理确保稳定性。
- 简洁高效:BlockingCollection 简化异步生产者-消费者模型,减少手动同步。
最佳实践
适用场景总结
| 用户取消 | UI 取消按钮 | cts.Cancel() |
| 超时控制 | 网络请求 | new CancellationTokenSource(TimeSpan) |
| 批量任务 | 并行下载 | Task.WhenAll + CancellationToken |
| 生产者-消费者 | 异步队列 | BlockingCollection + CancellationToken |
通过优化异步生产者-消费者模型并结合 CancellationTokenSource 的取消机制,开发者可以实现高效、可靠的异步任务取消,满足网络、文件处理和任务队列等场景的需求。




