C# 取消机制
什么是取消机制?
想象一下这个场景:你在网上下载一个大文件,突然发现下错了,点击"取消"按钮。你期望的是下载立即停止,而不是程序卡死或者继续在后台下载完。
C# 的取消机制就是解决这个问题的!它是一种协作式的取消方式,意思是:
-
不是强制终止:不像强行关闭程序那样粗暴
-
礼貌地请求停止:向正在执行的任务发送"请停止"的信号
-
任务自己决定如何停止:任务收到信号后,可以安全地保存状态、释放资源,然后优雅退出
创建和基本使用
cs
using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 创建 CancellationTokenSource
var 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("操作已被取消");
}
}
}
完整的生活化例子
让我们用一个更贴近生活的例子来解释:
场景:厨房做饭
cs
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 类
这是取消操作的发起者,负责创建和管理取消信号。
构造函数
cs
// 1. 默认构造函数 - 创建未取消的源
var cts1 = new CancellationTokenSource();
// 2. 带超时的构造函数 - 指定时间后自动取消
var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(5)); // 5秒后取消
var cts3 = new CancellationTokenSource(5000); // 5000毫秒后取消
主要属性
cs
public class CancellationTokenSourceProperties
{
public static void DemonstrateProperties()
{
var cts = new CancellationTokenSource();
// Token: 获取与此源关联的 CancellationToken
CancellationToken token = cts.Token;
Console.WriteLine($"Token: {token}");
// IsCancellationRequested: 检查是否已请求取消
bool isCancelled = cts.IsCancellationRequested;
Console.WriteLine($"是否已取消: {isCancelled}"); // 初始为 false
}
}
主要方法
cs
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();
}
}
静态方法
cs
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}"); // true
Console.WriteLine($"linkedCts 取消状态: {linkedCts.IsCancellationRequested}"); // true
}
}
2. CancellationToken 结构
这是取消操作的接收者,用于检测取消请求。
主要属性
cs
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
}
}
主要方法
cs
public class CancellationTokenMethods
{
public static void DemonstrateMethods()
{
var cts = new CancellationTokenSource();
var token = cts.Token;
// ThrowIfCancellationRequested(): 如果已请求取消,则抛出 OperationCanceledException
try
{
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:文件下载器
cs
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:数据库查询超时
cs
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 是预期的异常,应适当处理