CancellationToken 与超时
本教程共 100 篇 · 第 86 篇 · 更新于 2026-07-31 · 约 7 分钟阅读
86. CancellationToken 与超时
本节目标:学完能取消一个正在进行的异步任务,并给任务加上超时保护。
为什么需要取消
现实里任务常常要能中止:用户点了”取消下载”、网页请求超过三秒就该放弃、批量处理中途发现无需继续。
直接”杀线程”太粗暴,可能留下半成品状态。.NET 用协作式取消:任务定期检查”是否被要求停止”,自己干净地退出。
Note协作式取消(cooperative cancellation)的意思是:取消方发信号,任务方主动响应,而不是被硬掐断。
CancellationTokenSource 与 Token
CancellationTokenSource(简称 CTS)是”取消信号的发令枪”。它产出一个 CancellationToken,传给要取消的任务。
using CancellationTokenSource cts = new();
CancellationToken token = cts.Token;
Task work = DoWorkAsync(token);
cts.CancelAfter(2000); // 两秒后自动发取消信号
await work;
async Task DoWorkAsync(CancellationToken ct)
{
for (int i = 0; i < 10; i++)
{
ct.ThrowIfCancellationRequested(); // 被取消就抛异常
await Task.Delay(500, ct);
Console.WriteLine($"第 {i} 步");
}
}
cts.Cancel() 手动取消,CancelAfter(毫秒) 延时自动取消。
任务如何响应取消
任务内部靠两种方式感知取消:
- 调
token.ThrowIfCancellationRequested(),被取消时抛出OperationCanceledException。 - 把 token 传给支持取消的 API,如
Task.Delay(500, ct),它本身会在取消时结束。
Tip把 token 一路传进底层 API,比自己在循环里反复检查更自然,也更能及时中止等待中的 IO。
捕获取消异常
取消会抛 OperationCanceledException(或其子类 TaskCanceledException)。用 try/catch 区分”正常完成”与”被取消”。
try
{
await DoWorkAsync(token);
}
catch (OperationCanceledException)
{
Console.WriteLine("任务被取消了");
}
Note这是正常控制流,不是程序出错。捕获它,按”用户主动中止”来善后即可。
用 Task.Delay 实现超时
超时是取消的常见特例:超过给定时间还没好,就放弃。把 CancelAfter 配合任务即可。
using CancellationTokenSource cts = new();
cts.CancelAfter(1500); // 1.5 秒超时
try
{
await Task.Delay(3000, cts.Token); // 本要等 3 秒
Console.WriteLine("正常完成");
}
catch (TaskCanceledException)
{
Console.WriteLine("超时,已放弃");
}
Warning别用
Thread.Sleep做超时——它阻塞线程。用Task.Delay(..., token),既异步又不占线程。
多个任务共用一个令牌
一个 CTS 的 token 可以传给多个任务,一次取消全部。这在”关闭程序时中止所有进行中操作”很常见。
using CancellationTokenSource cts = new();
CancellationToken token = cts.Token;
Task a = DownloadAsync("a", token);
Task b = DownloadAsync("b", token);
// 需要时:cts.Cancel(); 两个一起停
await Task.WhenAll(a, b);
async Task DownloadAsync(string name, CancellationToken ct)
{
await Task.Delay(1000, ct);
Console.WriteLine($"{name} 下载逻辑");
}
新手易踩的坑
坑一:创建了 CTS 却忘了 Dispose,长期运行会漏资源。用 using 声明最省心。
坑二:不把 token 往下传,任务根本不知道要取消,Cancel 形同虚设。
坑三:吞掉取消异常却不区分,把”用户取消”当成”系统错误”报警。
Tip任何可能耗时的公开异步方法,都应接受一个
CancellationToken参数,这是 .NET 的通用契约。
取消是协作式的细节
强调一次:取消不会”强行杀掉”任务。任务代码必须自己检查 token 并主动退出,取消才生效。
async Task LoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
await Task.Delay(200, ct);
Console.WriteLine("工作中");
}
Console.WriteLine("收到取消,优雅收尾");
}
若任务里从不去碰 token,即便调了 cts.Cancel(),它也会一路跑到底——这是新手最常见的”取消无效”。
Tip把 token 传给
Task.Delay、HttpClient.GetAsync等原生 API,它们内部会帮你检查,比手动轮询更及时。
关联多个令牌:LinkedTokenSource
有时你想”任一条件触发就取消”。用 CreateLinkedTokenSource 把多个令牌连起来,任一取消则整体取消。
using CancellationTokenSource userCts = new();
using CancellationTokenSource timeoutCts = new();
using CancellationTokenSource linked =
CancellationTokenSource.CreateLinkedTokenSource(userCts.Token, timeoutCts.Token);
timeoutCts.CancelAfter(2000);
await WorkAsync(linked.Token);
async Task WorkAsync(CancellationToken ct)
{
await Task.Delay(5000, ct); // 用户取消或超时都会中止
}
NoteLinkedTokenSource 常用于”用户手动取消”叠加”系统超时”的双重保险。
实战:可取消的进度循环
把取消串进业务循环,用户随时能停,且中途状态可清理:
using CancellationTokenSource cts = new();
Task job = MonitorAsync(cts.Token);
cts.CancelAfter(3000); // 模拟 3 秒后用户取消
await job;
async Task MonitorAsync(CancellationToken ct)
{
for (int i = 0; i < 10; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(500, ct);
Console.WriteLine($"进度 {i * 10}%");
}
Console.WriteLine("全部完成");
}
Warning在循环里务必周期性检查 token。若某次计算特别久且不检查,取消信号要等那次算完才生效。
注册取消回调
除了在循环里轮询,还能用 token.Register 注册一个回调:一旦取消,立即执行(比如释放资源、记日志)。
using CancellationTokenSource cts = new();
cts.Token.Register(() => Console.WriteLine("已收到取消信号"));
cts.CancelAfter(1000);
await Task.Delay(2000, cts.Token);
回调在取消发生的线程上跑,适合做”清理动作”。注意回调本身要短平快,别在里面再长时间阻塞。
Note回调和轮询是两套响应机制:轮询适合循环体逐步走,回调适合”发生即处理”的边角清理。
超时实战:带超时的下载
把超时和真实异步 API 结合,超时后任务被取消,不再空等:
using CancellationTokenSource cts = new();
cts.CancelAfter(3000); // 3 秒超时
try
{
// 假设有个支持取消的异步下载
await DownloadAsync("https://example.com", cts.Token);
Console.WriteLine("下载成功");
}
catch (TaskCanceledException)
{
Console.WriteLine("超过 3 秒,放弃");
}
async Task DownloadAsync(string url, CancellationToken ct)
{
await Task.Delay(5000, ct); // 模拟需 5 秒,会被超时中止
}
Tip任何外部调用都该接受 token 并设超时。无超时的网络请求,一旦对方卡住,你的线程可能被吊到天荒地老。
取消后的资源清理
任务被取消常抛 OperationCanceledException。在 finally 里释放你打开的资源(文件、连接),保证不留半截状态。
async Task ProcessAsync(CancellationToken ct)
{
var stream = File.OpenRead("data.txt");
try
{
await stream.ReadAsync(new byte[1024], ct);
}
finally
{
stream.Dispose(); // 无论取消与否都关闭
}
}
Warning取消不是”什么都不管”。优雅退出意味着:停止新工作、收尾旧资源、给调用方明确信号。
一句话要点回顾
把取消与超时浓缩成几句,写代码时对照:
- 发信号用
CancellationTokenSource,收信号用它的Token。 - 任务里用
ThrowIfCancellationRequested()或把 token 传进底层 API。 - 超时 =
cts.CancelAfter(毫秒)+ 可取消的Task.Delay。 - 多个触发源用
CreateLinkedTokenSource关联。 - 记得
using释放 CTS,别漏资源。 - 取消是协作,令牌要一路传到底。
Tip养成习惯:凡是
async公开方法,第二个参数基本都该是CancellationToken,这是 .NET 生态的通用礼仪。
易混概念:取消 vs 中止
“取消(cancel)“和”中止(abort)“常被混用,但语义不同:
- 取消是协作式的:发信号,任务自己干净退出,资源可清理。
- 中止是强硬的:不管任务在干啥,强制停下(如旧的
Thread.Abort),容易留半成品、损坏状态。
.NET 的 CancellationToken 属于前者。这也是为什么没有”强制杀 Task”的安全 API——协作取消才可靠。
Warning切勿为了”立刻停”去想强杀线程的办法。协作取消虽要写点检查代码,却是唯一不埋雷的路径。
默认令牌与”不可取消”
当你写一个可取消的方法,但不想每次都传 token,可用 CancellationToken.None 表示”永不取消”。它和 default 等价。
async Task LoadAsync(CancellationToken ct = default)
{
await Task.Delay(100, ct); // 调用方不传就是 None,正常运行不可取消
}
Note公开异步方法把 token 参数默认设为
default(即 None),调用方既可传也可不传,兼容性最好。
为什么 .NET 选择 CancellationToken 这种设计
很多语言用”全局取消”或”抛异常中断”来停任务,但都难保证状态安全。.NET 把取消设计成一个显式传递的信号对象,而非隐式全局开关,好处有三:
- 显式清晰:谁需要响应取消,谁就在参数里收令牌,读代码一眼可见。
- 可组合:能叠加超时(
CancelAfter)、能关联多源(CreateLinkedTokenSource)。 - 原生支持:所有异步 API 都认它,不必自己轮询标志位。
把令牌当”护照”:你出门(发起任务)带着护照(token),边检(底层 API)随时能查验你是否被召回;召回信号由发令枪(CTS)发出。护照要随身带(一路传参),落在家里(没传下去)就作废。
Note这正是 .NET 异步生态的通用契约:公开异步方法第二个参数基本都是
CancellationToken。
CancellationToken 与其他取消机制对比
| 机制 | 风格 | 风险 |
|---|---|---|
Thread.Abort(已废弃) | 强制掐断 | 留半成品、损坏状态 |
共享 bool 标志位 | 协作,但需手动轮询 | 忘记检查就无效 |
CancellationToken | 协作,框架原生支持 | 几乎无,令牌要传到底 |
CancellationToken 的优势在于它是一等公民,所有原生异步 API(Task.Delay、HttpClient、Stream.ReadAsync)都认它,还能叠加超时、关联多个信号源。比自己维护 bool 标志位省心且更及时。
Warning切勿为了”立刻停”去想强杀线程的办法。协作取消虽要多写几句检查代码,却是唯一不埋雷的路径。
适用场景:何时该上取消
这些场景几乎都该支持取消:
- 用户能主动中止的操作(取消下载、停止导入)。
- 任何对外请求——都应带超时,防对方卡死拖垮你。
- 程序关闭时,需要中止所有进行中的任务。
- 批量处理中途可能无需继续的长任务。
Tip凡是
async公开方法,第二个参数基本都该是CancellationToken(默认default即不可取消)。这是 .NET 的通用礼仪,调用方既能传也能不传。
小结
CancellationTokenSource 发取消信号,CancellationToken 传给任务,任务用 ThrowIfCancellationRequested 或底层 API 响应。超时就是 CancelAfter + Task.Delay。记住:取消要协作,令牌要一路传到底。