-
C#中关于取消令牌(CancellationToken)——异步任务中断
第一部分:C#基础入门
取消令牌(CancellationToken)——异步任务中断
实例介绍
你有没有遇到过这样的场景?
下载一个10GB的大文件,下到一半不想下了,点击取消按钮,但程序还在后台偷偷下载,浪费流量和内存;
调用第三方API接口,等了10秒还没响应,想自动终止请求,避免一直阻塞;
并行处理多个任务,其中一个出错了,想让所有任务都停止运行,而不是继续浪费资源。
这些场景的核心需求都是优雅中断异步任务。而C#中的取vb.net教程C#教程python教程SQL教程access 2010教程
消令牌(CancellationToken) 就是为解决这类问题而生——它通过协作式取消机制,让任务和调用方“商量着来”,任务可以在关键节点检查取消信号,清理资源后再终止,避免资源泄漏。
需求分析
本节通过三个典型场景,带你掌握取消令牌的用法:
1.用户主动取消:WPF/WinForms中,用户点击“取消”按钮中断下载任务;
2.超时自动取消:API调用超过指定时间(比如5秒),自动终止请求;
3.多任务共享取消:多个并行任务共享同一个取消信号,一个取消则全部停止;
4.资源清理:任务取消时,确保文件流、网络连接等资源被正确释放。
代码实现
前置条件:
目标框架:.NET 6+;
场景1需WPF项目(模拟UI交互);
需引用System.Net.Http(HttpClient)。
场景1:用户主动取消(WPF示例)
错误写法(无取消机制,任务继续运行):
csharp
// WPF按钮点击事件(错误:无取消功能)
private async void BtnStartDownload_Click(object sender, RoutedEventArgs e)
{
try
{
await DownloadLargeFileAsync("https://example.com/largefile.zip");
MessageBox.Show("下载成功!");
}
catch (Exception ex)
{
MessageBox.Show($"下载失败:{ex.Message}");
}
}
// 无取消机制的下载方法
private async Task DownloadLargeFileAsync(string url)
{
using var client = new HttpClient();
var response = await client.GetAsync(url); // 无法中断
var content = await response.Content.ReadAsByteArrayAsync(); // 无法中断
await File.WriteAllBytesAsync("largefile.zip", content); // 无法中断
}
正确写法(用CancellationTokenSource支持取消):
csharp
using System.Net.Http;
using System.IO;
using System.Windows;
namespace CancelTokenDemo
{
public partial class MainWindow : Window
{
private CancellationTokenSource _cts; // 取消令牌源(生产者)
public MainWindow()
{
InitializeComponent();
}
// 开始下载按钮
private async void BtnStartDownload_Click(object sender, RoutedEventArgs e)
{
// 先取消之前的任务(避免多个任务并行)
_cts?.Cancel();
_cts = new CancellationTokenSource(); // 创建新的取消源
try
{
// 传递取消令牌(Token)给下载方法
await DownloadLargeFileAsync("https://example.com/largefile.zip", _cts.Token);
MessageBox.Show("下载成功!");
}
catch (OperationCanceledException)
{
MessageBox.Show("下载已取消");
}
catch (Exception ex)
{
MessageBox.Show($"下载失败:{ex.Message}");
}
finally
{
_cts.Dispose(); // 释放资源(必须!)
_cts = null;
}
}
// 取消下载按钮
private void BtnCancelDownload_Click(object sender, RoutedEventArgs e)
{
_cts?.Cancel(); // 触发取消信号
}
// 支持取消的下载方法
private async Task DownloadLargeFileAsync(string url, CancellationToken cancellationToken)
{
using var client = new HttpClient();
// 传递Token给GetAsync(HttpClient支持取消)
using var response = await client.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode(); // 检查HTTP响应是否成功
// 传递Token给ReadAsStreamAsync(逐流读取,节省内存)
using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
using var fileStream = new FileStream("largefile.zip", FileMode.Create, FileAccess.Write);
// 逐块读取,每块检查取消状态
var buffer = new byte[8192]; // 8KB缓冲区
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
// 检查取消(如果已取消,抛出OperationCanceledException)
cancellationToken.ThrowIfCancellationRequested();
await fileStream.WriteAsync(buffer, 0, bytesRead, cancellationToken);
// 更新进度条(示例):ProgressBar.Value = (stream.Position * 100) / response.Content.Headers.ContentLength;
}
}
}
}
场景2:超时自动取消(API调用示例)
csharp
// 超时自动取消:API调用超过5秒终止
private async Task CallWeatherApiWithTimeoutAsync()
{
using var cts = new CancellationTokenSource();
// 设置超时:5秒后自动触发取消
cts.CancelAfter(TimeSpan.FromSeconds(5));
try
{
var weather = await GetWeatherDataAsync("Beijing", cts.Token);
Console.WriteLine($"北京天气:{weather}");
}
catch (OperationCanceledException)
{
Console.WriteLine("API调用超时(>5秒)");
}
catch (Exception ex)
{
Console.WriteLine($"API调用失败:{ex.Message}");
}
}
// 支持取消的API调用方法
private async Task<string> GetWeatherDataAsync(string city, CancellationToken cancellationToken)
{
using var client = new HttpClient();
// 传递Token给GetAsync,超时会抛出OperationCanceledException
var response = await client.GetAsync($"https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q={city}", cancellationToken);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync(cancellationToken);
}
场景3:多任务共享取消令牌
csharp
// 多个并行任务共享取消信号
private async Task ParallelDownloadsWithCancelAsync()
{
using var cts = new CancellationTokenSource();
// 启动后台任务:监听用户输入,按任意键取消所有任务
_ = Task.Run(() =>
{
Console.WriteLine("按任意键取消所有下载任务...");
Console.ReadKey();
cts.Cancel(); // 触发全局取消
});
try
{
// 创建3个下载任务,共享同一个Token
var tasks = new List<Task>
{
DownloadFileAsync("file1.zip", cts.Token),
DownloadFileAsync("file2.zip", cts.Token),
DownloadFileAsync("file3.zip", cts.Token)
};
await Task.WhenAll(tasks); // 等待所有任务完成
Console.WriteLine("所有文件下载完成!");
}
catch (OperationCanceledException)
{
Console.WriteLine("所有下载任务已取消!");
}
}
// 模拟文件下载(支持取消)
private async Task DownloadFileAsync(string fileName, CancellationToken cancellationToken)
{
Console.WriteLine($"开始下载:{fileName}");
try
{
for (int i = 0; i < 10; i++) // 模拟10步下载
{
// 关键节点检查取消:如果已取消,抛出异常
cancellationToken.ThrowIfCancellationRequested();
// 模拟下载延迟(传递Token给Delay)
await Task.Delay(1000, cancellationToken);
Console.WriteLine($"{fileName}:下载进度 {i*10}%");
}
Console.WriteLine($"完成下载:{fileName}");
}
finally
{
Console.WriteLine($"清理资源:{fileName}(比如关闭文件流)");
}
}
逐行讲解
场景1:用户主动取消
CancellationTokenSource(_cts):
作为取消信号的生产者,负责生成CancellationToken(Token),并触发取消操作。
必须在finally块中Dispose,因为它持有定时器等资源,避免泄漏。
BtnStartDownload_Click:
先取消之前的任务(如果存在),防止多个任务并行。
创建新的_cts,传递_cts.Token给下载方法。
捕获OperationCanceledException处理用户取消。
DownloadLargeFileAsync:
传递cancellationToken给所有支持取消的异步方法(GetAsync、ReadAsStreamAsync、WriteAsync)。
用ThrowIfCancellationRequested在关键节点检查取消:如果已取消,立即抛出异常,终止任务。
逐块读取流(8KB缓冲区),既节省内存,又能在每块后检查取消。
场景2:超时自动取消
CancelAfter:设置超时时间,CancellationTokenSource会在指定时间后自动触发取消。
API调用中传递Token,超时后GetAsync会抛出OperationCanceledException,终止任务。
场景3:多任务共享取消
共享Token:3个下载任务使用同一个cts.Token,当用户按任意键时,所有任务都会收到取消信号。
finally块清理:任务取消时,finally块会执行资源清理(比如关闭文件流),避免泄漏。
基础知识拓展
- 取消令牌核心概念
| 概念 | 说明 |
|---|---|
| 协作式取消 | 任务必须主动检查取消信号,不能被强制终止(避免资源泄漏)。 |
| CancellationTokenSource | 取消信号的生产者:生成Token、触发取消(Cancel()/CancelAfter())。 |
| CancellationToken | 取消信号的消费者:用于检查取消状态(IsCancellationRequested/ThrowIfCancellationRequested)。 |
| 不可变性 | Token一旦生成,状态不可修改,只能传递给任务(安全线程共享)。 |
| 资源释放 | CancellationTokenSource必须Dispose,否则会泄漏定时器等资源。 |
-
取消令牌的正确用法
传递Token:所有支持取消的异步方法(如Task.Delay、HttpClient、Stream操作)必须传递Token。
关键节点检查:在任务的耗时操作前、循环迭代中、资源操作后检查取消状态。
异常处理:OperationCanceledException是预期异常,要捕获并处理(比如提示用户取消成功)。
注册回调:用cancellationToken.Register(() => { /* 清理操作 */ })在取消时执行资源清理(如关闭数据库连接)。 -
为什么不用Thread.Abort?
Thread.Abort会强制终止线程,可能导致:
资源泄漏(文件流未关闭、数据库连接未释放);
数据损坏(比如正在写入文件时被终止);
不可预测的异常(线程在任意位置被中断)。
而取消令牌是协作式的,任务可以自己清理资源后再终止,更安全可靠。 -
取消令牌与async/await的结合
await的方法如果支持Token,一定要传递(比如await Task.Delay(1000, token));
await会自动传播取消信号:如果Token已取消,await会立即抛出OperationCanceledException;
异步流(IAsyncEnumerable)中,用[EnumeratorCancellation]属性传递Token(参考上一节)。
总结
取消令牌是C#异步编程中优雅中断任务的核心工具,关键要点:
1.协作式取消:任务必须主动检查取消信号(ThrowIfCancellationRequested);
2.生产者-消费者模型:CancellationTokenSource生成Token,Token用于检查取消;
3.资源安全:取消时用finally块清理资源,避免泄漏;
4.异常处理:捕获OperationCanceledException处理预期的取消;
5.多任务共享:多个任务可共享同一个Token,实现全局取消。
掌握取消令牌,你就能写出更健壮、更用户友好的异步代码——让用户随时可以中断任务,让程序在超时后自动清理,避免资源浪费!
本站原创,转载请注明出处:https://www.xin3721.com/ArticlecSharp/c49448.html










