1. 认识 CancellationToken
假设你遇到这样一种情况:你在进行一个长时间的文件下载,然后突然想到,你的网络套餐有限,流量宝贵。或者你的应用用户搞不清楚,启动了某个计算,后来觉得没必要了——他希望点击“取消”按钮,不用等待程序优雅地完成。现代应用要保持响应性,就必须能随时接收停止信号,正确中断异步操作。
为此,.NET 提供了一整套取消基础设施——CancellationToken。
CancellationToken(字面意思“取消令牌”)是一个特殊对象,你可以在长时间运行的操作中传递它。任何时候,你都可以“设置标志”——发出取消信号,操作需要周期性检查这个标志,并在检测到取消请求时尽可能优雅地终止。
它是怎么工作的?(简要)
- 有一个
CancellationTokenSource对象,它“生成”token并能取消它们。 CancellationToken本身是“信号旗”,可以分发给多个操作。- 操作会定期检查token:如果检测到取消请求,就退出。
比喻一下:你是老板(你就是 CancellationTokenSource)。你给员工发“工作牌”——CancellationToken。当你觉得该全部取消时,就升起红旗——所有持有“工作牌”的员工马上撤退,留下一桌未吃完的饭菜。
2. 如何使用 CancellationToken
创建取消源 (CancellationTokenSource)
var cts = new CancellationTokenSource();
获取token (CancellationToken)
CancellationToken token = cts.Token;
将token传入异步方法
大部分.NET的标准异步方法都支持传入 CancellationToken 参数。例如,HttpClient.GetAsync、Stream.ReadAsync、Task.Delay 等。
示例——带取消的延时:
await Task.Delay(10000, token); // 等待10秒,但可以取消!
请求取消(比如按钮点击或定时器触发)
cts.Cancel(); // 所有收到这个token的操作都会知道取消了
在方法内部检查token
在自己的方法中(尤其是耗时长、循环多的场景),需要定期检查取消标志,并在检测到取消时抛出 OperationCanceledException:
token.ThrowIfCancellationRequested();
或者直接检查属性:
if (token.IsCancellationRequested)
{
// 释放资源,退出方法
}
3. 示例:给我们的学习应用加上取消功能
假设我们有个应用,从网站下载数据。现在要加个功能,让用户可以随时取消下载。
基础异步下载示例
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class Downloader
{
public async Task DownloadAsync(string url)
{
var client = new HttpClient();
string content = await client.GetStringAsync(url); // 不支持取消
Console.WriteLine("下载完成!");
}
}
加入 CancellationToken
public async Task DownloadAsync(string url, CancellationToken token)
{
var client = new HttpClient();
string content = await client.GetStringAsync(url, token); // 现在支持取消了!
Console.WriteLine("下载完成!");
}
由调用端控制取消
static async Task Main(string[] args)
{
var downloader = new Downloader();
var cts = new CancellationTokenSource();
Console.WriteLine("请输入要下载的URL:");
string url = Console.ReadLine();
var downloadTask = downloader.DownloadAsync(url, cts.Token);
Console.WriteLine("按任意键取消下载...");
Console.ReadKey();
cts.Cancel(); // 发送取消信号
try
{
await downloadTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("用户取消了下载!");
}
}
就这么简单!用户可以在任何时候中断操作。
4. CancellationTokenSource 和方法的交互
flowchart TD
A["用户代码(Main)"] -- 创建 --> B["CancellationTokenSource"]
B -- 生成 --> C["CancellationToken"]
C -- 传递 --> D["异步操作"]
A -- 调用 Cancel() --> B
D -- 定期检查 --> C
C -- 通知取消 --> D
D -- 抛出异常或结束 --> A
5. 取消的处理
当操作收到取消token时,有两种行为:
.NET 方法会自动抛出异常
如果你调用的标准方法(如 Stream.ReadAsync、HttpClient.GetAsync、Task.Delay)支持传入token,一旦调用 Cancel(),它们会自动抛出 OperationCanceledException。你只需捕获这个异常即可。
自定义异步代码
如果你自己实现“耗时”操作,比如循环处理或复杂计算,责任在你——要定期检查 token.IsCancellationRequested(或调用 token.ThrowIfCancellationRequested()),以便正确响应取消请求。
示例:带手动取消检测的“长时间”操作
public async Task CalculatePrimesAsync(int max, CancellationToken token)
{
for (int i = 2; i < max; i++)
{
token.ThrowIfCancellationRequested(); // 检查取消
if (IsPrime(i))
{
Console.WriteLine($"素数:{i}");
await Task.Delay(100, token); // 给点“休息”时间(支持取消)
}
}
Console.WriteLine("计算完成!");
}
private bool IsPrime(int n)
{
for (int i = 2; i <= Math.Sqrt(n); i++)
if (n % i == 0) return false;
return true;
}
6. 一些实用细节
支持 CancellationToken 的方法和类
| 类/方法 | 支持 CancellationToken? |
示例 |
|---|---|---|
Task.Delay |
✔ | await Task.Delay(1000, token); |
HttpClient.GetAsync |
✔ | await client.GetAsync(url, token); |
Stream.ReadAsync |
✔ | await stream.ReadAsync(buffer, 0, len, token); |
Task.Run |
✔ | Task.Run(() => ..., token); |
File.ReadAllTextAsync |
✔ | await File.ReadAllTextAsync(path, token); |
Thread.Sleep |
✖ | (不支持;建议用 Task.Delay) |
| 你自己写的方法 | ✔(如果支持的话!) | MyLongMethod(token) |
可取消操作的生命周期示意
sequenceDiagram
participant 用户
participant Main
participant CancellationTokenSource
participant 异步操作
用户->>Main: 启动操作
Main->>CancellationTokenSource: 创建 CTS
Main->>异步操作: 启动并传递 CancellationToken
用户->>Main: 点击“取消”
Main->>CancellationTokenSource: 调用 Cancel()
异步操作->>异步操作: 发现取消(\nIsCancellationRequested)
异步操作-->>Main: 抛出 OperationCanceledException
Main->>用户: 显示“操作已取消”消息
实际应用场景
- UI应用: 用户想中断长时间加载、计算、文件操作(比如关闭窗口或取消按钮)
- 服务器端: 客户端断开连接时,立即取消请求处理,节省资源
- 大数据处理: 任务可能耗时很长,要提供停止计算或迁移的途径
- 硬件交互: 扫描、打印等操作需要支持紧急取消
7. 常见错误和注意事项
有些开发者会高估 CancellationToken 的能力,觉得取消是瞬间完成的。实际上,取消只是操作的“自愿”行为。如果操作没有定期检查token,任何“宇宙力量”都无法强制它停止(除非直接终止整个进程,但那是极端情况)。
另外,取消并不保证“状态”会被保存。如果你在写文件或数据库,必须自己处理事务回滚或资源释放。Token 只是个信号,具体怎么反应由代码决定。
还有个细节:如果把“已取消”的token传给方法,它会立即抛出异常。工作中要注意避免重复使用已取消的token,除非你明确知道要这样做。
GO TO FULL VERSION