1. 介绍
当你启动一个异步(或耗时)操作时,用户(或别的代码)可能突然决定:“停!不需要了!停下来!”。比如用户决定中断一个超大文件的下载、关闭了程序窗口,或者改变主意不再从大数据库里查数据。没有取消支持的话,你的程序可能会继续执行并白白消耗资源——这对用户和机器都不够友好。
常见的取消场景:
- 取消文件下载或取消数据提交。
- 按用户要求快速退出复杂的数据处理流程。
- 在长期任务突然不再需要时暂停/停止该任务。
取消是你让应用更友好、更响应、更节省资源的秘密武器。
如何在 .NET 中取消异步操作?
在 .NET 里取消耗时任务的概念是“取消令牌”(CancellationToken)。这是一个特殊对象,会传到任务的各个部分。如果有人请求取消——令牌会立刻把这个消息通知程序中所有关心它的部分。实际效果就像一面红旗:谁先看到谁停止。
在 .NET 中这个机制通过两个主要类来实现:
- CancellationTokenSource — 创建并管理取消令牌。
- CancellationToken — 传给异步方法以便它们可以被取消。
重要:取消令牌本身不会强制中断代码执行,它只是发出“信号”——具体怎么响应这个信号由你的应用决定。
2. 创建取消令牌并取消任务
我们用一个简单例子来看它如何工作(扩展我们的小型控制台教学程序)。
示例:支持取消的简单异步操作
using System;
using System.Threading;
using System.Threading.Tasks;
namespace DemoApp
{
class Program
{
static async Task Main()
{
// 创建取消令牌源
CancellationTokenSource cts = new CancellationTokenSource();
// 启动异步任务
Task longRunningTask = DoWorkAsync(cts.Token);
Console.WriteLine("按任意键以取消操作...");
Console.ReadKey();
// 请求取消
cts.Cancel();
try
{
await longRunningTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("操作已被取消!");
}
}
// 支持取消的异步方法
static async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
// 检查取消信号
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"执行步骤 {i + 1}/10...");
await Task.Delay(1000); // 延迟 1 秒
}
Console.WriteLine("操作成功完成!");
}
}
}
这是怎么工作的?
- 我们创建了 CancellationTokenSource(变量名cts),从它那里拿到令牌(cts.Token)。
- 把令牌传给异步操作。
- 在 DoWorkAsync() 内用 ThrowIfCancellationRequested() 定期检查令牌。如果用户请求取消——该方法会抛出 OperationCanceledException,任务就会中止。
- 在 Main() 等待键盘输入后调用 cts.Cancel() 来“发信号”要求停止操作。
如果你不检查 cancellationToken.IsCancellationRequested 或不调用 ThrowIfCancellationRequested(),任务会照常继续运行——令牌只是个信息化的旗子。
3. CancellationToken:内部是怎样的?以及一点小技巧
取消令牌是一个可以方便地在方法和任务之间传递的对象,这带来了很大的灵活性:
- 同一个令牌可以在多个异步或同步操作中复用。
- 如果多个操作使用同一个 CancellationTokenSource 的令牌,就可以实现“群组取消”,一次取消影响所有任务。
- 取消令牌是非侵入式的:即便你忽略它,代码也能如常执行。
怎么检查令牌:在哪儿、如何检查?
在逻辑上合适的地方检查是否有“取消标志”是必须的:循环内、每个耗时步骤、阶段切换处等。
// 在任何需要检查的点
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("操作已取消!退出...");
return;
}
// 或者这样(简短并抛出异常)
cancellationToken.ThrowIfCancellationRequested();
通常使用 ThrowIfCancellationRequested() ——它会抛出一个专门的异常,调用方可以捕获它来处理取消逻辑。
4. 标准库的异步方法
很多 .NET 类和方法(尤其是异步方法)开箱即支持 CancellationToken。应该利用这些 API 来“正确”地停止操作。
下面是一个异步读取文件的例子:
using System.IO;
using System.Threading;
using System.Threading.Tasks;
class FileDemo
{
public static async Task ReadFileWithCancelAsync(string filePath, CancellationToken cancellationToken)
{
using FileStream stream = File.OpenRead(filePath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
// 处理数据...
// 如果取消令牌被请求,ReadAsync 本身会抛出 OperationCanceledException
}
}
}
想了解更多关于 FileStream.ReadAsync 和 CancellationToken 的内容,可以查看官方文档。
5. 有用的细节
超时也是一种取消!
你可以设置在超时后自动取消操作。为此可以“编程”一个带超时的取消令牌:
// 创建带超时的 CancellationTokenSource(例如 5 秒)
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
经过 5 秒后令牌会自动“升起”,在下一次检查时所有使用该令牌的操作都会停止。这在你不想无限等待时非常方便。
取消发生时会怎样?
当你在令牌源上调用 Cancel(),所有使用该令牌并检查其状态的方法都会收到通知。但如果你的代码不检查令牌——取消就不会发生。
常见错误:忘了把令牌传递给所有异步和耗时操作。那样一部分操作会被取消,而另一部分会继续运行,造成不一致。
可视化:取消是怎么流转的
sequenceDiagram
participant Main as 主线程
participant CTS as CancellationTokenSource
participant Task as 异步任务
Main->>CTS: 创建 CTS,获取令牌
Main->>Task: 把令牌传给异步任务
Note over Task: 任务定期检查令牌
Main->>CTS: 调用 Cancel()
CTS-->>Task: 令牌状态变为“已取消”
Task-->>Main: 抛出 OperationCanceledException
Main->>Main: 捕获异常并结束工作
取消异步操作通常用在哪儿?
- 异步下载和对服务器的请求:网络差或用户不想等时可以取消。
- 大型计算:可以按超时或用户要求停止。
- 网络操作、文件处理、后台大集合处理等场景。
到这里对取消异步操作的介绍就结束了——现在你的应用不仅会更快,还会更懂得“关心”用户!
6. 建议和常见错误
别忘了把取消令牌传给所有支持 cancellation 的方法和调用。如果哪儿没传——操作可能会“挂住”而无法停止。
要定期检查令牌——特别是在长循环、文件处理或大数据传输时。使用 IsCancellationRequested 或 ThrowIfCancellationRequested()。
不要尝试在外部“强行”终止线程或任务:取消令牌是礼貌的“请停止”,不是用来强制结束线程的武器。
标准库函数比如 ReadAsync、Delay、HttpClient.SendAsync 等都已经支持通过令牌取消。务必利用这些功能!
处理取消时应专门捕获 OperationCanceledException ——这是表示按请求正确取消的专用异常。
GO TO FULL VERSION