首页 / C# 入门教程 / CancellationToken 与超时

C# 入门教程

CancellationToken 与超时

本教程共 100 篇 · 第 86 篇 · 更新于 2026-07-31 · 约 7 分钟阅读

C#C# 入门教程编程语言异步编程CancellationToken

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(毫秒) 延时自动取消。

任务如何响应取消

任务内部靠两种方式感知取消:

  1. token.ThrowIfCancellationRequested(),被取消时抛出 OperationCanceledException
  2. 把 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.DelayHttpClient.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); // 用户取消或超时都会中止
}
Note

LinkedTokenSource 常用于”用户手动取消”叠加”系统超时”的双重保险。

实战:可取消的进度循环

把取消串进业务循环,用户随时能停,且中途状态可清理:

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 把取消设计成一个显式传递的信号对象,而非隐式全局开关,好处有三:

  1. 显式清晰:谁需要响应取消,谁就在参数里收令牌,读代码一眼可见。
  2. 可组合:能叠加超时(CancelAfter)、能关联多源(CreateLinkedTokenSource)。
  3. 原生支持:所有异步 API 都认它,不必自己轮询标志位。

把令牌当”护照”:你出门(发起任务)带着护照(token),边检(底层 API)随时能查验你是否被召回;召回信号由发令枪(CTS)发出。护照要随身带(一路传参),落在家里(没传下去)就作废。

Note

这正是 .NET 异步生态的通用契约:公开异步方法第二个参数基本都是 CancellationToken

CancellationToken 与其他取消机制对比

机制风格风险
Thread.Abort(已废弃)强制掐断留半成品、损坏状态
共享 bool 标志位协作,但需手动轮询忘记检查就无效
CancellationToken协作,框架原生支持几乎无,令牌要传到底

CancellationToken 的优势在于它是一等公民,所有原生异步 API(Task.DelayHttpClientStream.ReadAsync)都认它,还能叠加超时、关联多个信号源。比自己维护 bool 标志位省心且更及时。

Warning

切勿为了”立刻停”去想强杀线程的办法。协作取消虽要多写几句检查代码,却是唯一不埋雷的路径。

适用场景:何时该上取消

这些场景几乎都该支持取消:

  1. 用户能主动中止的操作(取消下载、停止导入)。
  2. 任何对外请求——都应带超时,防对方卡死拖垮你。
  3. 程序关闭时,需要中止所有进行中的任务。
  4. 批量处理中途可能无需继续的长任务。
Tip

凡是 async 公开方法,第二个参数基本都该是 CancellationToken(默认 default 即不可取消)。这是 .NET 的通用礼仪,调用方既能传也能不传。

小结

CancellationTokenSource 发取消信号,CancellationToken 传给任务,任务用 ThrowIfCancellationRequested 或底层 API 响应。超时就是 CancelAfter + Task.Delay。记住:取消要协作,令牌要一路传到底。