C# 取消机制(CancellationTokenSource/CancellationToken)
C# 取消机制
什么是取消机制?
想象一下这个场景:你在网上下载一个大文件,突然发现下错了,点击"取消"按钮。你期望的是下载立即停止,而不是程序卡死或者继续在后台下载完。
C# 的取消机制就是解决这个问题的!它是一种协作式的取消方式,意思是:
-
不是强制终止:不像强行关闭程序那样粗暴
-
礼貌地请求停止:向正在执行的任务发送"请停止"的信号
-
任务自己决定如何停止:任务收到信号后,可以安全地保存状态、释放资源,然后优雅退出
创建和基本使用
using System;
using System.Threading;
using System.Threading.Tasks;class Program
{static void Main(){// 创建 CancellationTokenSourcevar cts = new CancellationTokenSource();// 启动一个可取消的任务Task.Run(() => DoWork(cts.Token));// 等待用户输入后取消Console.ReadLine();cts.Cancel(); // 取消Console.WriteLine("取消信号已发送");Console.ReadLine();}static void DoWork(CancellationToken cancellationToken){try{for (int i = 0; i < 100; i++){// 检查取消请求cancellationToken.ThrowIfCancellationRequested();Console.WriteLine($"工作进度: {i}%");Thread.Sleep(100);}}catch (OperationCanceledException){Console.WriteLine("操作已被取消");}}
}
完整的生活化例子
让我们用一个更贴近生活的例子来解释:
场景:厨房做饭
using System;
using System.Threading;
using System.Threading.Tasks;class Kitchen
{static async Task Main(){// 厨师开始做饭var chef = new Chef();// 客人有一个"取消按钮"(如果等不及可以取消订单)var guestCancellation = new CancellationTokenSource();// 厨房也有一个超时限制(30分钟没做完自动取消)var kitchenTimeout = new CancellationTokenSource(TimeSpan.FromMinutes(30));// 组合两个取消源:客人取消 OR 厨房超时using var combinedCancellation = CancellationTokenSource.CreateLinkedTokenSource(guestCancellation.Token, kitchenTimeout.Token);try{// 开始做饭!Console.WriteLine("厨师开始准备餐点...");await chef.CookMealAsync(combinedCancellation.Token);Console.WriteLine("餐点准备完成!");}catch (OperationCanceledException) when (guestCancellation.Token.IsCancellationRequested){Console.WriteLine("客人取消了订单");}catch (OperationCanceledException) when (kitchenTimeout.Token.IsCancellationRequested){Console.WriteLine("厨房超时,无法完成订单");}// 模拟客人取消Console.WriteLine("\n按回车键模拟客人取消订单...");Console.ReadLine();guestCancellation.Cancel(); // 取消}
}class Chef
{public async Task CookMealAsync(CancellationToken cancellationToken){string[] steps = { "准备食材", "切菜", "烹饪", "摆盘", "上菜" };foreach (var step in steps){// 检查是否收到取消信号cancellationToken.ThrowIfCancellationRequested();Console.WriteLine($"正在: {step}");await Task.Delay(2000, cancellationToken); // 模拟每个步骤需要2秒}}
}
核心类详解
1. CancellationTokenSource 类
这是取消操作的发起者,负责创建和管理取消信号。
构造函数
// 1. 默认构造函数 - 创建未取消的源
var cts1 = new CancellationTokenSource();// 2. 带超时的构造函数 - 指定时间后自动取消
var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // 5秒后取消
var cts3 = new CancellationTokenSource(5000); // 5000毫秒后取消
主要属性
public class CancellationTokenSourceProperties
{public static void DemonstrateProperties(){var cts = new CancellationTokenSource();// Token: 获取与此源关联的 CancellationTokenCancellationToken token = cts.Token;Console.WriteLine($"Token: {token}");// IsCancellationRequested: 检查是否已请求取消bool isCancelled = cts.IsCancellationRequested;Console.WriteLine($"是否已取消: {isCancelled}"); // 初始为 false}
}
主要方法
public class CancellationTokenSourceMethods
{public static void DemonstrateMethods(){var cts = new CancellationTokenSource();// Cancel(): 请求取消操作Console.WriteLine("调用 Cancel() 方法...");cts.Cancel();Console.WriteLine($"取消状态: {cts.IsCancellationRequested}"); // true// CancelAfter(): 在指定时间后自动取消var delayedCts = new CancellationTokenSource();delayedCts.CancelAfter(TimeSpan.FromSeconds(3)); // 3秒后自动取消Console.WriteLine($"3秒后将自动取消: {delayedCts.IsCancellationRequested}"); // false// Dispose(): 释放资源cts.Dispose();delayedCts.Dispose();}
}
静态方法
public class CancellationTokenSourceStaticMethods
{public static void DemonstrateStaticMethods(){var cts1 = new CancellationTokenSource();var cts2 = new CancellationTokenSource();// CreateLinkedTokenSource(): 创建链接的取消源// 当任意一个源取消时,链接的源也会取消using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);Console.WriteLine("当 cts1 或 cts2 取消时,linkedCts 也会自动取消");// 测试链接效果cts1.Cancel();Console.WriteLine($"cts1 取消状态: {cts1.IsCancellationRequested}"); // trueConsole.WriteLine($"linkedCts 取消状态: {linkedCts.IsCancellationRequested}"); // true}
}
2. CancellationToken 结构
这是取消操作的接收者,用于检测取消请求。
主要属性
public class CancellationTokenProperties
{public static void DemonstrateProperties(CancellationToken token){// IsCancellationRequested: 是否已请求取消Console.WriteLine($"是否请求取消: {token.IsCancellationRequested}");// CanBeCanceled: 此令牌能否被取消Console.WriteLine($"能否被取消: {token.CanBeCanceled}");// WaitHandle: 获取等待取消信号的可等待句柄Console.WriteLine($"WaitHandle: {token.WaitHandle}");// None: 静态属性,返回空的无法取消的令牌var emptyToken = CancellationToken.None;Console.WriteLine($"空令牌能否取消: {emptyToken.CanBeCanceled}"); // false}
}
主要方法
public class CancellationTokenMethods
{public static void DemonstrateMethods(){var cts = new CancellationTokenSource();var token = cts.Token;// ThrowIfCancellationRequested(): 如果已请求取消,则抛出 OperationCanceledExceptiontry{token.ThrowIfCancellationRequested(); // 此时不会抛出异常Console.WriteLine("没有取消请求,继续执行...");cts.Cancel(); // 请求取消token.ThrowIfCancellationRequested(); // 这里会抛出异常}catch (OperationCanceledException){Console.WriteLine("捕获到 OperationCanceledException");}// Register(): 注册取消时的回调函数var callbackCts = new CancellationTokenSource();var callbackToken = callbackCts.Token;// 注册回调 - 当取消时自动执行CancellationTokenRegistration registration = callbackToken.Register(() => {Console.WriteLine("取消回调被执行!");Console.WriteLine("执行清理操作...");});Console.WriteLine("注册了取消回调");callbackCts.Cancel(); // 触发回调// 可以取消注册registration.Dispose();Console.WriteLine("回调已取消注册");}
}
实际应用场景
场景1:文件下载器
public class FileDownloader
{public async Task DownloadFileAsync(string url, string savePath, IProgress<int> progress = null,CancellationToken cancellationToken = default){using var httpClient = new HttpClient();try{// 开始下载using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);response.EnsureSuccessStatusCode();// 获取文件总大小var totalBytes = response.Content.Headers.ContentLength ?? -1;using var stream = await response.Content.ReadAsStreamAsync();using var fileStream = new FileStream(savePath, FileMode.Create);var buffer = new byte[8192];int bytesRead;long totalRead = 0;// 读取数据并写入文件while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0){cancellationToken.ThrowIfCancellationRequested();await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);totalRead += bytesRead;// 报告进度if (totalBytes > 0 && progress != null){int percentage = (int)((totalRead * 100) / totalBytes);progress.Report(percentage);}}Console.WriteLine("下载完成!");}catch (OperationCanceledException){// 如果文件已部分下载,删除不完整的文件if (File.Exists(savePath))File.Delete(savePath);Console.WriteLine("下载已被取消");throw;}}
}// 使用示例
class Program
{static async Task Main(){var downloader = new FileDownloader();var cts = new CancellationTokenSource();// 创建进度报告器var progress = new Progress<int>(percent =>{Console.WriteLine($"下载进度: {percent}%");});// 启动下载任务var downloadTask = downloader.DownloadFileAsync("https://example.com/largefile.zip","largefile.zip",progress,cts.Token);// 模拟用户取消(5秒后)_ = Task.Delay(5000).ContinueWith(_ => {Console.WriteLine("用户取消了下载");cts.Cancel();});try{await downloadTask;}catch (OperationCanceledException){Console.WriteLine("下载任务已成功取消");}}
}
场景2:数据库查询超时
public class DatabaseService
{public async Task<List<Product>> SearchProductsAsync(string keyword,TimeSpan timeout,CancellationToken externalToken = default){// 创建查询超时using var timeoutCts = new CancellationTokenSource(timeout);// 组合令牌:外部取消 OR 超时取消using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(externalToken, timeoutCts.Token);try{// 模拟数据库查询await Task.Delay(2000, linkedCts.Token); // 假设查询需要2秒// 这里应该是真实的数据库查询代码// using var connection = new SqlConnection(connectionString);// await connection.OpenAsync(linkedCts.Token);// 执行查询...return new List<Product>{new Product { Id = 1, Name = $"{keyword} 产品1" },new Product { Id = 2, Name = $"{keyword} 产品2" }};}catch (OperationCanceledException) when (timeoutCts.Token.IsCancellationRequested){throw new TimeoutException($"数据库查询超时,超过 {timeout.TotalSeconds} 秒");}}
}public class Product
{public int Id { get; set; }public string Name { get; set; }
}
属性方法总结表
CancellationTokenSource 主要成员
| 成员 | 类型 | 说明 |
|---|---|---|
Token | 属性 | 获取关联的 CancellationToken |
IsCancellationRequested | 属性 | 检查是否已请求取消 |
Cancel() | 方法 | 请求取消操作 |
CancelAfter(TimeSpan) | 方法 | 在指定时间后自动取消 |
CancelAfter(int) | 方法 | 在指定毫秒数后自动取消 |
Dispose() | 方法 | 释放资源 |
CreateLinkedTokenSource() | 静态方法 | 创建链接的取消源 |
CancellationToken 主要成员
| 成员 | 类型 | 说明 |
|---|---|---|
IsCancellationRequested | 属性 | 是否已请求取消 |
CanBeCanceled | 属性 | 此令牌能否被取消 |
WaitHandle | 属性 | 获取等待取消信号的句柄 |
None | 静态属性 | 空令牌(无法取消) |
ThrowIfCancellationRequested() | 方法 | 如果已取消则抛出异常 |
Register(Action) | 方法 | 注册取消回调 |
Equals() | 方法 | 比较两个令牌 |
GetHashCode() | 方法 | 获取哈希码 |
重要注意事项
-
资源释放:始终对 CancellationTokenSource 调用 Dispose
-
回调顺序:多个回调按注册顺序执行
-
线程安全:这些类型都是线程安全的
-
性能考虑:频繁检查 IsCancellationRequested 可能有性能影响
-
异常处理:OperationCanceledException 是预期的异常,应适当处理
