一、什么是 CancellationToken?
1.1 为什么要学习 CancellationToken?
在开发应用程序时,我们经常会遇到一些需要长时间运行的操作,比如:
-
从网络下载大文件
-
处理大量数据
-
在运动控制系统中,电机从当前位置运动到目标位置
-
执行复杂的计算任务
这些操作可能需要几秒、几分钟甚至更长时间才能完成。但是如果用户想中途取消这个操作,或者发生了紧急情况需要立即停止,我们应该怎么做呢?
在传统的编程方式中,如果我们想停止一个正在运行的线程,可能会使用 Thread.Abort() 这样的方法。但这种方式非常危险,因为它会强制终止线程,可能会导致:
-
数据不一致(线程正在修改数据时被突然打断)
-
资源泄漏(文件、数据库连接等没有正确关闭)
-
程序状态不可预测
为了安全地取消操作,.NET 提供了 CancellationToken 机制。这是一种协作式的取消方式,不是强制终止,而是通知操作"我想取消你,请你自己安全地停止"。
1.2 CancellationToken 的工作原理
想象一下这样的场景:
传统方式(不推荐):
老板 → 员工正在工作 → 突然拔掉电源(暴力终止)
结果:员工可能正在处理重要文件,直接断电导致数据损坏
CancellationToken 方式(推荐):
老板 → 员工正在工作 → 老板说"请停止工作"
员工 → 检查到停止信号 → 保存当前工作 → 清理现场 → 安全停止
结果:工作有序停止,没有数据损坏
在 CancellationToken 机制中:
-
CancellationTokenSource(令牌源):相当于"老板",负责发起取消请求
-
CancellationToken(令牌):相当于"通知",传递给执行任务的代码,告诉它是否应该停止
-
执行任务的代码:相当于"员工",定期检查令牌状态,如果收到取消信号就安全地停止
1.3 核心概念详解
CancellationTokenSource(令牌源)
-
作用:这是取消操作的源头,只有通过它才能发起取消请求
-
特点 :可以主动调用
Cancel()方法来取消操作 -
生命周期:使用后应该及时释放资源(实现了 IDisposable 接口)
-
比喻:就像一个广播站,只有它能发送"取消"的广播信号
CancellationToken(令牌)
-
作用:被传递给异步方法,用于检查是否收到取消信号
-
特点:本身不能主动取消,只能被动接收取消状态
-
不可变性:一旦创建就不能修改,这是线程安全的
-
比喻:就像一个收音机,只能接收信号,不能发送信号
CancellationTokenRegistration(取消注册)
-
作用:用于注册取消时的回调函数
-
特点:当取消发生时,会自动执行注册的回调
-
比喻:就像设置了闹钟,当"取消"信号到来时自动执行某些操作
1.4 CancellationToken 的使用场景
在运动控制卡编程中,CancellationToken 尤其重要:
-
紧急停止:操作员按下急停按钮,需要立即停止所有电机运动
-
用户中断:用户点击"取消"按钮,停止当前的运动任务
-
超时保护:运动超过预设时间仍未到达目标位置,自动停止
-
多轴同步停止:多轴联动时,一个轴异常需要停止所有轴
-
资源清理:取消运动时,需要关闭相关硬件设备,释放资源
没有 CancellationToken,我们很难实现这些功能,或者实现的代码会很复杂且不安全。
二、基本使用
2.1 创建和使用 CancellationToken
让我们通过一个完整的例子来理解如何使用 CancellationToken。这个例子模拟了一个需要 10 秒才能完成的工作,但我们在 5 秒后就想取消它。
cs
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;
async Task DoWorkAsync(CancellationToken ct)
{
for(int i=0;i<10;i++)
{
ct.ThrowIfCancellationRequested();//检查请求是否取消
Console.WriteLine($"working...{i+1}/10");
await Task.Delay(1000,ct);
}
Console.WriteLine("work end!");
}
var task = DoWorkAsync(token);
await Task.Delay(5000);
cts.Cancel();
try
{
await task;
}
catch(OperationCanceledException)
{
Console.WriteLine("wrok stop");
}

关键要点:
-
ThrowIfCancellationRequested()是主动检查,如果不调用它,任务可能不会及时响应取消 -
传递 token 给
Task.Delay()可以在等待期间响应取消 -
取消通过抛出异常来实现,这是 .NET 的标准做法
-
OperationCanceledException是预期的异常,不是程序错误
2.2 CancellationTokenSource 的常见方法
CancellationTokenSource 提供了多个有用的方法,让我们来看看它们的作用和使用场景。
cs
// 创建一个 CancellationTokenSource
CancellationTokenSource cts = new CancellationTokenSource();
// 方法1:立即取消
// 这是最常用的方法,立即发出取消信号
// 通常在用户点击"取消"按钮或发生错误时使用
cts.Cancel();
Console.WriteLine("已发出取消信号");
// 方法2:延迟后自动取消
// 方式A:使用 TimeSpan,更易读
cts.CancelAfter(TimeSpan.FromSeconds(5));
// 5秒后会自动发出取消信号,无需手动调用 Cancel()
// 方式B:使用毫秒数,更精确
cts.CancelAfter(5000);
// 5000毫秒(5秒)后自动取消
// 应用场景:防止任务长时间卡死
// 例如:网络请求最多等待10秒,超时自动取消
// 例如:电机运动最多等待30秒到达目标位置,超时自动停止
// 方法3:创建关联多个 token 的新 token
// 这是高级用法,允许同时响应多个取消源
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
// 创建一个关联的 token
// 只要 cts1 或 cts2 中任一被取消,linkedCts.Token 都会收到取消信号
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts1.Token, cts2.Token);
// 应用场景:
// 1. 全局取消 + 用户取消
// cts1.Token:用户点击取消按钮
// cts2.Token:系统关闭时自动取消
// linkedCts.Token:同时响应这两种取消
// 2. 运动控制场景
// cts1.Token:用户点击"停止"按钮
// cts2.Token:限位开关触发(硬件信号)
// linkedCts.Token:任一情况发生都停止运动
// 方法4:释放资源
// CancellationTokenSource 实现了 IDisposable 接口
// 使用完毕后应该调用 Dispose() 释放资源
cts.Dispose();
// 推荐做法:使用 using 语句自动释放
using var ctsAuto = new CancellationTokenSource();
// 使用 ctsAuto
// 离开作用域时自动调用 Dispose()
详细说明 CancelAfter 的使用:
cs
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromSeconds(3));
var token = cts.Token;
try
{
await Task.Delay(5000,token);
Console.WriteLine("Work end");
}
catch(OperationCanceledException)
{
if(token.IsCancellationRequested)
Console.WriteLine("work stop");
else
Console.WriteLine("work cancel");
}
详细说明关联 Token 的使用:
cs
// 场景:运动控制系统,需要同时响应多个停止信号
class MotionSystem
{
private CancellationTokenSource _userCancelSource; // 用户取消
private CancellationTokenSource _emergencyStopSource; // 急停信号
private CancellationTokenSource _linkedSource; // 关联的源
public void Start()
{
_userCancelSource = new CancellationTokenSource();
_emergencyStopSource = new CancellationTokenSource();
// 创建关联的 token,任一信号都触发取消
_linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
_userCancelSource.Token,
_emergencyStopSource.Token
);
// 使用关联的 token 启动运动
StartMotionAsync(_linkedSource.Token);
}
// 用户点击"停止"按钮
public void UserStop()
{
_userCancelSource?.Cancel();
Console.WriteLine("用户请求停止");
}
// 硬件急停信号触发
public void EmergencyStop()
{
_emergencyStopSource?.Cancel();
Console.WriteLine("急停信号触发!");
}
}
三、在异步方法中使用 CancellationToken
3.1 异步方法接收 CancellationToken
在 .NET 的异步编程最佳实践中,所有可能长时间运行的异步方法都应该接受 CancellationToken 参数。这是一个约定,也是编写健壮代码的关键。
cs
// 好的做法:所有异步方法都应接受 CancellationToken
// 参数命名约定:通常使用 ct 或 cancellationToken
// 参数位置约定:通常是最后一个参数
// 默认值:使用 default(CancellationToken),允许调用方选择不传递
async Task DownloadFileAsync(string url, CancellationToken ct = default)
{
try
{
// 在耗时操作中传递 token
// 这样可以让操作本身支持取消
using var httpClient = new HttpClient();
// GetAsync 方法支持 CancellationToken
// 如果在等待响应时收到取消信号,会立即取消请求
var response = await httpClient.GetAsync(url, ct);
// ReadAsStringAsync 也支持 CancellationToken
// 如果在读取内容时收到取消信号,会立即停止读取
var content = await response.Content.ReadAsStringAsync(ct);
Console.WriteLine($"下载完成,内容长度: {content.Length}");
}
catch (OperationCanceledException)
{
// 捕获取消异常,这是正常的取消流程
// 不应该记录为错误
Console.WriteLine("下载已取消");
}
catch (HttpRequestException ex)
{
// 捕获网络错误,这是真正的错误
Console.WriteLine($"网络错误: {ex.Message}");
}
}
为什么要在参数中传递 CancellationToken?
-
允许调用方控制取消:调用方可以决定是否允许取消,以及何时取消
-
遵循.NET惯例:这是.NET异步编程的标准做法
-
支持取消传播:允许取消信号从上层方法传递到下层方法
-
提高响应性:让整个调用链都能快速响应取消请求
3.2 检查取消状态的两种方式
在异步方法中,有两种方式来检查是否应该取消操作。理解它们的区别很重要,因为它们适用于不同的场景。
方式1:ThrowIfCancellationRequested() - 抛出异常
cs
async Task ProcessDataAsync(CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
// 方式1:检查并抛出异常
// 如果已请求取消,抛出 OperationCanceledException
// 优点:代码简洁,自动停止执行
// 缺点:需要使用 try-catch 处理异常
ct.ThrowIfCancellationRequested();
// 执行一些工作
await DoSomeWorkAsync(i, ct);
}
}
工作原理:
-
调用
ThrowIfCancellationRequested()检查 token 的状态 -
如果
IsCancellationRequested为true,抛出OperationCanceledException -
抛出异常后,后续代码不会执行,方法立即退出
-
异常会向上传播,直到被某个 try-catch 捕获
适用场景:
-
想要立即停止操作,不需要做任何清理工作
-
希望取消信号以异常的形式向上传播
-
方法调用链中已经有人处理
OperationCanceledException
方式2:IsCancellationRequested - 自行处理
cs
async Task ProcessDataAsync(CancellationToken ct)
{
List<int> processedData = new List<int>();
for (int i = 0; i < 1000; i++)
{
// 方式2:检查后自行处理
// 只检查状态,不抛出异常
// 优点:可以执行清理逻辑,控制退出方式
// 缺点:需要自己编写退出逻辑
if (ct.IsCancellationRequested)
{
Console.WriteLine("检测到取消请求,开始清理资源...");
// 可以在这里执行清理工作
// 例如:保存已处理的数据、关闭连接、释放资源等
await SavePartialResultsAsync(processedData);
// 优雅地退出方法
return; // 正常返回,不抛出异常
}
// 执行工作
int result = await DoSomeWorkAsync(i, ct);
processedData.Add(result);
}
}
工作原理:
-
检查
IsCancellationRequested属性 -
如果为
true,执行自定义的清理逻辑 -
使用
return正常退出方法,不抛出异常 -
调用方不知道方法是被取消的,只认为方法正常完成
适用场景:
-
需要在取消时执行特定的清理工作
-
希望部分完成的结果能够被保存
-
不希望以异常的形式通知调用方
-
需要记录取消事件
两种方式的对比
| 特性 | ThrowIfCancellationRequested() | IsCancellationRequested |
|---|---|---|
| 是否抛出异常 | 是 | 否 |
| 代码简洁度 | 高 | 中等 |
| 能否执行清理逻辑 | 只能在 finally 块中 | 可以在检测后立即执行 |
| 调用方是否知道取消 | 是(通过异常) | 否(正常返回) |
| 推荐使用场景 | 大多数情况 | 需要特殊清理逻辑时 |
实际应用示例
让我们通过一个实际的运动控制例子来理解两种方式的区别:
cs
// 场景1:简单的数据处理,不需要清理
async Task SimpleProcessAsync(CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
ct.ThrowIfCancellationRequested(); // ✅ 使用 ThrowIfCancellationRequested
await ProcessItemAsync(i, ct);
}
}
// 场景2:需要保存中间结果
async Task ProcessWithSavingAsync(CancellationToken ct)
{
var results = new List<int>();
for (int i = 0; i < 1000; i++)
{
if (ct.IsCancellationRequested) // ✅ 使用 IsCancellationRequested
{
await SavePartialResultsAsync(results); // 保存已完成的部分
return;
}
int result = await ProcessItemAsync(i, ct);
results.Add(result);
}
}
// 场景3:需要通知调用方取消状态
async Task ProcessWithNotificationAsync(CancellationToken ct)
{
try
{
for (int i = 0; i < 1000; i++)
{
ct.ThrowIfCancellationRequested(); // ✅ 使用 ThrowIfCancellationRequested
await ProcessItemAsync(i, ct);
}
}
catch (OperationCanceledException)
{
// 重新抛出,让调用方知道操作被取消
throw; // ✅ 抛出异常通知调用方
}
}
// 场景4:需要同时处理取消和异常
async Task ProcessWithBothAsync(CancellationToken ct)
{
var results = new List<int>();
try
{
for (int i = 0; i < 1000; i++)
{
if (ct.IsCancellationRequested) // ✅ 使用 IsCancellationRequested
{
await CleanupAsync(results);
throw new OperationCanceledException(ct); // 手动抛出异常
}
int result = await ProcessItemAsync(i, ct);
results.Add(result);
}
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// 处理其他异常
await HandleErrorAsync(ex);
throw;
}
}
3.3 在 WinForms 中使用
在 Windows 窗体应用程序中使用 CancellationToken 是非常常见的场景。这通常涉及用户界面(UI)和后台任务的交互。让我们通过一个完整的例子来学习如何正确实现。
cs
public partial class MainForm : Form
{
// 字段:存储 CancellationTokenSource 和当前任务
private CancellationTokenSource _cts;
private Task _currentTask;
public MainForm()
{
InitializeComponent();
// 初始化时,取消按钮应该禁用
btnCancel.Enabled = false;
}
// 开始按钮点击事件
private async void btnStart_Click(object sender, EventArgs e)
{
// 步骤1:创建新的 CancellationTokenSource
// 每次开始新任务时,都应该创建一个新的实例
_cts = new CancellationTokenSource();
// 步骤2:更新 UI 状态
// 禁用开始按钮,启用取消按钮,防止重复点击
btnStart.Enabled = false;
btnCancel.Enabled = true;
progressBar1.Value = 0;
lblStatus.Text = "准备开始...";
try
{
// 步骤3:启动长时间运行的操作
// 将 token 传递给方法,使其支持取消
_currentTask = LongRunningOperation(_cts.Token);
// 步骤4:等待操作完成
// 使用 await 关键字,不会阻塞 UI 线程
await _currentTask;
// 步骤5:操作正常完成
lblStatus.Text = "操作完成!";
MessageBox.Show("操作完成!", "成功", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (OperationCanceledException)
{
// 步骤6:操作被取消
// 这是正常情况,不是错误
lblStatus.Text = "操作已取消";
MessageBox.Show("操作已取消", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
catch (Exception ex)
{
// 步骤7:处理其他异常
lblStatus.Text = "发生错误";
MessageBox.Show($"发生错误:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// 步骤8:清理资源,恢复 UI 状态
// finally 块中的代码总是会执行,无论是否发生异常
btnStart.Enabled = true;
btnCancel.Enabled = false;
// 释放 CancellationTokenSource
_cts?.Dispose();
_cts = null;
_currentTask = null;
}
}
// 取消按钮点击事件
private void btnCancel_Click(object sender, EventArgs e)
{
// 步骤1:发出取消信号
// 调用 Cancel() 会通知所有持有该 token 的任务
_cts?.Cancel();
// 步骤2:更新 UI
// 禁用取消按钮,防止重复点击
btnCancel.Enabled = false;
lblStatus.Text = "正在取消...";
}
// 长时间运行的操作
// 这个方法会在后台线程执行,不会阻塞 UI
private async Task LongRunningOperation(CancellationToken ct)
{
// 模拟处理 100 个项目
for (int i = 0; i < 100; i++)
{
// 步骤1:检查是否请求取消
// 这是在后台线程执行的,不会阻塞 UI
ct.ThrowIfCancellationRequested();
// 步骤2:执行一些工作
// 这里用 Task.Delay 模拟耗时操作
await Task.Delay(100, ct);
// 步骤3:更新 UI(重要!)
// 由于这个方法在后台线程运行,不能直接访问 UI 控件
// 必须使用 Invoke 或 BeginInvoke 切换到 UI 线程
this.Invoke((MethodInvoker)delegate
{
// 更新进度条
progressBar1.Value = i + 1;
// 更新状态标签
lblStatus.Text = $"处理中... {i + 1}/100";
// 可以添加其他 UI 更新
lblProgress.Text = $"{i + 1}%";
});
}
}
}
重要概念解释:
1. 为什么需要字段变量?
cs
// 使用字段而不是局部变量的原因:
private CancellationTokenSource _cts; // ✅ 字段,可以在多个方法中访问
// ❌ 如果使用局部变量,其他方法无法访问
private async void btnStart_Click(object sender, EventArgs e)
{
var cts = new CancellationTokenSource(); // 局部变量
await LongRunningOperation(cts.Token);
}
// ❌ btnCancel_Click 无法访问上面的 cts
private void btnCancel_Click(object sender, EventArgs e)
{
// cts 在这里不可见!
// cts?.Cancel(); // 编译错误
}
2. 为什么每次都要创建新的 CancellationTokenSource?
cs
// ❌ 错误:重复使用同一个实例
private CancellationTokenSource _cts = new CancellationTokenSource();
private async void btnStart_Click(object sender, EventArgs e)
{
// 如果用户点击两次"开始",会出现问题
// 第一次点击已经开始,第二次点击又创建任务
// 当第二次点击后点击"取消",两次任务都会被取消
_cts.Cancel(); // 这会影响所有使用过这个 token 的任务
}
// ✅ 正确:每次开始新任务时创建新实例
private async void btnStart_Click(object sender, EventArgs e)
{
// 每次点击"开始",都创建一个新的 CancellationTokenSource
// 这样不同的任务之间互不影响
_cts = new CancellationTokenSource();
}
3. 为什么要使用 Invoke 更新 UI?
在 WinForms 中,有一条重要规则:只有创建控件的线程(UI 线程)才能访问该控件。
cs
private async Task LongRunningOperation(CancellationToken ct)
{
for (int i = 0; i < 100; i++)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(100, ct);
// ❌ 错误:直接访问 UI 控件
// 这个方法在后台线程运行,不能直接访问 UI 控件
progressBar1.Value = i + 1; // 会抛出 InvalidOperationException
// ✅ 正确:使用 Invoke 切换到 UI 线程
this.Invoke((MethodInvoker)delegate
{
// 这段代码会在 UI 线程执行
progressBar1.Value = i + 1;
lblStatus.Text = $"处理中... {i + 1}/100";
});
}
}
Invoke 的工作原理:
-
Invoke会将代码排队到 UI 线程的消息队列中 -
UI 线程在处理消息时执行这段代码
-
Invoke是同步的,会等待 UI 线程执行完成 -
还可以使用
BeginInvoke,它是异步的,不会等待
四、高级用法
4.1 注册取消回调
有时候我们需要在取消发生时执行一些特定的操作,比如清理资源、关闭设备连接、记录日志等。CancellationToken 提供了 Register 方法,允许我们注册回调函数,当取消发生时会自动执行这些回调。
基本用法
cs
// 创建 CancellationTokenSource
CancellationTokenSource cts = new CancellationTokenSource();
// 注册取消时的回调
// Register 方法返回一个 CancellationTokenRegistration 对象
// 这个对象可以用于注销回调
CancellationTokenRegistration registration = cts.Token.Register(() =>
{
Console.WriteLine("执行清理工作...");
// 在这里执行清理操作
// 例如:关闭文件、释放资源、关闭设备连接等
CleanupResources();
// 例如:记录日志
LogCancellation();
});
// 执行一些工作
await DoSomeWorkAsync(cts.Token);
// 触发取消
cts.Cancel();
// 手动注销回调(如果需要)
// 如果回调已经执行,注销没有影响
registration.Dispose();
详细说明:
1. Register 的工作原理
cs
// Register 方法的签名
// public CancellationTokenRegistration Register(Action callback)
// 回调函数会在以下情况被执行:
// 1. 调用 CancellationTokenSource.Cancel()
// 2. 调用 CancellationTokenSource.CancelAfter() 超时后自动取消
// 3. 关联的令牌源被取消(使用 CreateLinkedTokenSource 创建的)
// 回调函数的执行时机:
// - 在 Cancel() 调用的线程中同步执行
// - 如果注册了多个回调,会按照注册顺序依次执行
// - 回调函数执行期间,其他线程可能继续运行
2. 注册多个回调
cs
CancellationTokenSource cts = new CancellationTokenSource();
// 可以注册多个回调
var registration1 = cts.Token.Register(() =>
{
Console.WriteLine("回调1: 关闭数据库连接");
CloseDatabaseConnection();
});
var registration2 = cts.Token.Register(() =>
{
Console.WriteLine("回调2: 清理临时文件");
CleanTempFiles();
});
var registration3 = cts.Token.Register(() =>
{
Console.WriteLine("回调3: 记录取消日志");
LogCancellation();
});
// 当调用 Cancel() 时,所有回调都会执行
cts.Cancel();
// 输出:
// 回调1: 关闭数据库连接
// 回调2: 清理临时文件
// 回调3: 记录取消日志
// 清理注册对象
registration1.Dispose();
registration2.Dispose();
registration3.Dispose();
3. 回调的执行顺序
cs
CancellationTokenSource cts = new CancellationTokenSource();
// 注册回调的顺序就是执行顺序
cts.Token.Register(() => Console.WriteLine("第1个回调"));
cts.Token.Register(() => Console.WriteLine("第2个回调"));
cts.Token.Register(() => Console.WriteLine("第3个回调"));
cts.Cancel();
// 输出:
// 第1个回调
// 第2个回调
// 第3个回调
4. 回调中抛出异常
cs
CancellationTokenSource cts = new CancellationTokenSource();
// 如果回调中抛出异常,会阻止后续回调执行
cts.Token.Register(() =>
{
Console.WriteLine("回调1开始");
throw new Exception("回调1出错"); // 抛出异常
});
cts.Token.Register(() =>
{
Console.WriteLine("回调2"); // 不会执行!
});
try
{
cts.Cancel();
}
catch (AggregateException ex)
{
Console.WriteLine($"捕获到异常: {ex.InnerExceptions[0].Message}");
}
// 输出:
// 回调1开始
// 捕获到异常: 回调1出错
可以在回调中直接catch到异常,这样即使出现了异常,也不会阻止后面的回调
Register 方法的关键要点:
-
回调是同步执行的 :回调会在调用
Cancel()的线程中同步执行 -
可以注册多个回调:按照注册顺序依次执行
-
应该捕获异常:回调中应该捕获异常,避免影响其他回调
-
可以注销回调 :通过返回的
CancellationTokenRegistration对象可以注销回调 -
适合用于资源清理:在取消时自动清理资源,避免资源泄漏
5.2 传递 CancellationToken 到多个异步操作
在实际应用中,我们经常需要并行执行多个任务,并且希望能够一次性取消所有任务。通过将同一个 CancellationToken 传递给所有任务,可以轻松实现这个需求。
cs
async Task ComplexOperationAsync(CancellationToken ct)
{
var tasks = new List<Task>();
// 并行执行多个任务,共享同一个取消令牌
// 这样,当令牌被取消时,所有任务都会收到通知
tasks.Add(Task.Run(() => Task1(ct), ct));
tasks.Add(Task.Run(() => Task2(ct), ct));
tasks.Add(Task.Run(() => Task3(ct), ct));
try
{
// 等待所有任务完成
await Task.WhenAll(tasks);
Console.WriteLine("所有任务完成");
}
catch (OperationCanceledException)
{
Console.WriteLine("某个或所有任务被取消");
throw;
}
catch (AggregateException ex)
{
// 捕获多个任务的异常
Console.WriteLine($"捕获到 {ex.InnerExceptions.Count} 个异常");
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($" - {innerEx.Message}");
}
throw;
}
}
// 各个任务的实现
async Task Task1(CancellationToken ct)
{
for (int i = 0; i < 10; i++)
{
ct.ThrowIfCancellationRequested();
Console.WriteLine($"Task1 运行中... {i + 1}/10");
await Task.Delay(200, ct);
}
Console.WriteLine("Task1 完成");
}
async Task Task2(CancellationToken ct)
{
for (int i = 0; i < 15; i++)
{
ct.ThrowIfCancellationRequested();
Console.WriteLine($"Task2 运行中... {i + 1}/15");
await Task.Delay(150, ct);
}
Console.WriteLine("Task2 完成");
}
async Task Task3(CancellationToken ct)
{
for (int i = 0; i < 20; i++)
{
ct.ThrowIfCancellationRequested();
Console.WriteLine($"Task3 运行中... {i + 1}/20");
await Task.Delay(100, ct);
}
Console.WriteLine("Task3 完成");
}
使用示例:
cs
var cts = new CancellationTokenSource();
// 启动复杂操作
var operationTask = ComplexOperationAsync(cts.Token);
// 等待3秒后取消
await Task.Delay(3000);
Console.WriteLine("取消所有任务...");
cts.Cancel();
try
{
await operationTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("操作已取消");
}
输出示例:
cs
Task1 运行中... 1/10
Task2 运行中... 1/15
Task3 运行中... 1/20
Task1 运行中... 2/10
Task2 运行中... 2/15
Task3 运行中... 2/20
Task1 运行中... 3/10
Task2 运行中... 3/15
Task3 运行中... 3/20
取消所有任务...
某个或所有任务被取消
操作已取消
5.3 超时控制
超时控制是 CancellationToken 的一个重要应用场景。在运动控制系统中,我们经常需要设置超时,防止某个操作长时间卡死。
方式1:使用 CancellationTokenSource 的构造函数
cs
// 方式1:使用带超时的构造函数
// 创建一个会在指定时间后自动取消的 CancellationTokenSource
async Task<T> WithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout)
{
// 创建一个会在 timeout 时间后自动取消的 CancellationTokenSource
using var cts = new CancellationTokenSource(timeout);
try
{
// 等待任务完成
return await task;
}
catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested)
{
// 任务自己取消了,不是超时
// 这种情况下,异常是由任务本身抛出的,而不是超时
throw;
}
catch (OperationCanceledException)
{
// cts.Token.IsCancellationRequested 为 true
// 说明是超时导致的取消
throw new TimeoutException($"操作超时({timeout.TotalSeconds}秒)");
}
}
方式2:使用 Task.WhenAny 实现超时
cs
// 方式2:使用 Task.WhenAny 比较原任务和超时任务
async Task<T> WithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout)
{
// 创建超时任务
var timeoutTask = Task.Delay(timeout);
// 等待任一任务完成
var completedTask = await Task.WhenAny(task, timeoutTask);
// 如果超时任务先完成,抛出超时异常
if (completedTask == timeoutTask)
{
throw new TimeoutException($"操作超时({timeout.TotalSeconds}秒)");
}
// 否则返回原任务的结果
return await task;
}
方式3:结合 CancellationToken 和 Task.WhenAny
cs
// 方式3:结合使用 CancellationToken 和 Task.WhenAny
// 这样可以真正取消原任务,而不只是超时
async Task<T> WithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout)
{
// 创建超时的 CancellationTokenSource
using var cts = new CancellationTokenSource(timeout);
try
{
// 使用 Task.WhenAny 等待任一任务完成
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, cts.Token));
// 如果原任务先完成,返回结果
if (completedTask == task)
{
return await task;
}
// 如果超时,抛出异常
throw new TimeoutException($"操作超时({timeout.TotalSeconds}秒)");
}
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
{
// 确认是超时导致的取消
throw new TimeoutException($"操作超时({timeout.TotalSeconds}秒)");
}
}