C#使用Cancellation取消異步任務(wù)的全過程
一、什么是 CancellationToken?
1.1 為什么要學(xué)習(xí) CancellationToken?
在開發(fā)應(yīng)用程序時,我們經(jīng)常會遇到一些需要長時間運行的操作,比如:
- 從網(wǎng)絡(luò)下載大文件
- 處理大量數(shù)據(jù)
- 在運動控制系統(tǒng)中,電機從當(dāng)前位置運動到目標位置
- 執(zhí)行復(fù)雜的計算任務(wù)
這些操作可能需要幾秒、幾分鐘甚至更長時間才能完成。但是如果用戶想中途取消這個操作,或者發(fā)生了緊急情況需要立即停止,我們應(yīng)該怎么做呢?
在傳統(tǒng)的編程方式中,如果我們想停止一個正在運行的線程,可能會使用 Thread.Abort() 這樣的方法。但這種方式非常危險,因為它會強制終止線程,可能會導(dǎo)致:
- 數(shù)據(jù)不一致(線程正在修改數(shù)據(jù)時被突然打斷)
- 資源泄漏(文件、數(shù)據(jù)庫連接等沒有正確關(guān)閉)
- 程序狀態(tài)不可預(yù)測
為了安全地取消操作,.NET 提供了 CancellationToken 機制。這是一種協(xié)作式的取消方式,不是強制終止,而是通知操作"我想取消你,請你自己安全地停止"。
1.2 CancellationToken 的工作原理
想象一下這樣的場景:
傳統(tǒng)方式(不推薦):
老板 → 員工正在工作 → 突然拔掉電源(暴力終止) 結(jié)果:員工可能正在處理重要文件,直接斷電導(dǎo)致數(shù)據(jù)損壞
CancellationToken 方式(推薦):
老板 → 員工正在工作 → 老板說"請停止工作" 員工 → 檢查到停止信號 → 保存當(dāng)前工作 → 清理現(xiàn)場 → 安全停止 結(jié)果:工作有序停止,沒有數(shù)據(jù)損壞
在 CancellationToken 機制中:
- CancellationTokenSource(令牌源):相當(dāng)于"老板",負責(zé)發(fā)起取消請求
- CancellationToken(令牌):相當(dāng)于"通知",傳遞給執(zhí)行任務(wù)的代碼,告訴它是否應(yīng)該停止
- 執(zhí)行任務(wù)的代碼:相當(dāng)于"員工",定期檢查令牌狀態(tài),如果收到取消信號就安全地停止
1.3 核心概念詳解
CancellationTokenSource(令牌源)
- 作用:這是取消操作的源頭,只有通過它才能發(fā)起取消請求
- 特點:可以主動調(diào)用
Cancel()方法來取消操作 - 生命周期:使用后應(yīng)該及時釋放資源(實現(xiàn)了 IDisposable 接口)
- 比喻:就像一個廣播站,只有它能發(fā)送"取消"的廣播信號
CancellationToken(令牌)
- 作用:被傳遞給異步方法,用于檢查是否收到取消信號
- 特點:本身不能主動取消,只能被動接收取消狀態(tài)
- 不可變性:一旦創(chuàng)建就不能修改,這是線程安全的
- 比喻:就像一個收音機,只能接收信號,不能發(fā)送信號
CancellationTokenRegistration(取消注冊)
- 作用:用于注冊取消時的回調(diào)函數(shù)
- 特點:當(dāng)取消發(fā)生時,會自動執(zhí)行注冊的回調(diào)
- 比喻:就像設(shè)置了鬧鐘,當(dāng)"取消"信號到來時自動執(zhí)行某些操作
1.4 CancellationToken 的使用場景
在運動控制卡編程中,CancellationToken 尤其重要:
- 緊急停止:操作員按下急停按鈕,需要立即停止所有電機運動
- 用戶中斷:用戶點擊"取消"按鈕,停止當(dāng)前的運動任務(wù)
- 超時保護:運動超過預(yù)設(shè)時間仍未到達目標位置,自動停止
- 多軸同步停止:多軸聯(lián)動時,一個軸異常需要停止所有軸
- 資源清理:取消運動時,需要關(guān)閉相關(guān)硬件設(shè)備,釋放資源
沒有 CancellationToken,我們很難實現(xiàn)這些功能,或者實現(xiàn)的代碼會很復(fù)雜且不安全。
二、基本使用
2.1 創(chuàng)建和使用 CancellationToken
讓我們通過一個完整的例子來理解如何使用 CancellationToken。這個例子模擬了一個需要 10 秒才能完成的工作,但我們在 5 秒后就想取消它。
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");
}
關(guān)鍵要點:
ThrowIfCancellationRequested()是主動檢查,如果不調(diào)用它,任務(wù)可能不會及時響應(yīng)取消- 傳遞 token 給
Task.Delay()可以在等待期間響應(yīng)取消 - 取消通過拋出異常來實現(xiàn),這是 .NET 的標準做法
OperationCanceledException是預(yù)期的異常,不是程序錯誤
2.2 CancellationTokenSource 的常見方法
CancellationTokenSource 提供了多個有用的方法,讓我們來看看它們的作用和使用場景。
// 創(chuàng)建一個 CancellationTokenSource
CancellationTokenSource cts = new CancellationTokenSource();
// 方法1:立即取消
// 這是最常用的方法,立即發(fā)出取消信號
// 通常在用戶點擊"取消"按鈕或發(fā)生錯誤時使用
cts.Cancel();
Console.WriteLine("已發(fā)出取消信號");
// 方法2:延遲后自動取消
// 方式A:使用 TimeSpan,更易讀
cts.CancelAfter(TimeSpan.FromSeconds(5));
// 5秒后會自動發(fā)出取消信號,無需手動調(diào)用 Cancel()
// 方式B:使用毫秒數(shù),更精確
cts.CancelAfter(5000);
// 5000毫秒(5秒)后自動取消
// 應(yīng)用場景:防止任務(wù)長時間卡死
// 例如:網(wǎng)絡(luò)請求最多等待10秒,超時自動取消
// 例如:電機運動最多等待30秒到達目標位置,超時自動停止
// 方法3:創(chuàng)建關(guān)聯(lián)多個 token 的新 token
// 這是高級用法,允許同時響應(yīng)多個取消源
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
// 創(chuàng)建一個關(guān)聯(lián)的 token
// 只要 cts1 或 cts2 中任一被取消,linkedCts.Token 都會收到取消信號
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts1.Token, cts2.Token);
// 應(yīng)用場景:
// 1. 全局取消 + 用戶取消
// cts1.Token:用戶點擊取消按鈕
// cts2.Token:系統(tǒng)關(guān)閉時自動取消
// linkedCts.Token:同時響應(yīng)這兩種取消
// 2. 運動控制場景
// cts1.Token:用戶點擊"停止"按鈕
// cts2.Token:限位開關(guān)觸發(fā)(硬件信號)
// linkedCts.Token:任一情況發(fā)生都停止運動
// 方法4:釋放資源
// CancellationTokenSource 實現(xiàn)了 IDisposable 接口
// 使用完畢后應(yīng)該調(diào)用 Dispose() 釋放資源
cts.Dispose();
// 推薦做法:使用 using 語句自動釋放
using var ctsAuto = new CancellationTokenSource();
// 使用 ctsAuto
// 離開作用域時自動調(diào)用 Dispose()詳細說明 CancelAfter 的使用:
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");
}詳細說明關(guān)聯(lián) Token 的使用:
// 場景:運動控制系統(tǒng),需要同時響應(yīng)多個停止信號
class MotionSystem
{
private CancellationTokenSource _userCancelSource; // 用戶取消
private CancellationTokenSource _emergencyStopSource; // 急停信號
private CancellationTokenSource _linkedSource; // 關(guān)聯(lián)的源
public void Start()
{
_userCancelSource = new CancellationTokenSource();
_emergencyStopSource = new CancellationTokenSource();
// 創(chuàng)建關(guān)聯(lián)的 token,任一信號都觸發(fā)取消
_linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
_userCancelSource.Token,
_emergencyStopSource.Token
);
// 使用關(guān)聯(lián)的 token 啟動運動
StartMotionAsync(_linkedSource.Token);
}
// 用戶點擊"停止"按鈕
public void UserStop()
{
_userCancelSource?.Cancel();
Console.WriteLine("用戶請求停止");
}
// 硬件急停信號觸發(fā)
public void EmergencyStop()
{
_emergencyStopSource?.Cancel();
Console.WriteLine("急停信號觸發(fā)!");
}
}三、在異步方法中使用 CancellationToken
3.1 異步方法接收 CancellationToken
在 .NET 的異步編程最佳實踐中,所有可能長時間運行的異步方法都應(yīng)該接受 CancellationToken 參數(shù)。這是一個約定,也是編寫健壯代碼的關(guān)鍵。
// 好的做法:所有異步方法都應(yīng)接受 CancellationToken
// 參數(shù)命名約定:通常使用 ct 或 cancellationToken
// 參數(shù)位置約定:通常是最后一個參數(shù)
// 默認值:使用 default(CancellationToken),允許調(diào)用方選擇不傳遞
async Task DownloadFileAsync(string url, CancellationToken ct = default)
{
try
{
// 在耗時操作中傳遞 token
// 這樣可以讓操作本身支持取消
using var httpClient = new HttpClient();
// GetAsync 方法支持 CancellationToken
// 如果在等待響應(yīng)時收到取消信號,會立即取消請求
var response = await httpClient.GetAsync(url, ct);
// ReadAsStringAsync 也支持 CancellationToken
// 如果在讀取內(nèi)容時收到取消信號,會立即停止讀取
var content = await response.Content.ReadAsStringAsync(ct);
Console.WriteLine($"下載完成,內(nèi)容長度: {content.Length}");
}
catch (OperationCanceledException)
{
// 捕獲取消異常,這是正常的取消流程
// 不應(yīng)該記錄為錯誤
Console.WriteLine("下載已取消");
}
catch (HttpRequestException ex)
{
// 捕獲網(wǎng)絡(luò)錯誤,這是真正的錯誤
Console.WriteLine($"網(wǎng)絡(luò)錯誤: {ex.Message}");
}
}為什么要在參數(shù)中傳遞 CancellationToken?
- 允許調(diào)用方控制取消:調(diào)用方可以決定是否允許取消,以及何時取消
- 遵循.NET慣例:這是.NET異步編程的標準做法
- 支持取消傳播:允許取消信號從上層方法傳遞到下層方法
- 提高響應(yīng)性:讓整個調(diào)用鏈都能快速響應(yīng)取消請求
3.2 檢查取消狀態(tài)的兩種方式
在異步方法中,有兩種方式來檢查是否應(yīng)該取消操作。理解它們的區(qū)別很重要,因為它們適用于不同的場景。
方式1:ThrowIfCancellationRequested() - 拋出異常
async Task ProcessDataAsync(CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
// 方式1:檢查并拋出異常
// 如果已請求取消,拋出 OperationCanceledException
// 優(yōu)點:代碼簡潔,自動停止執(zhí)行
// 缺點:需要使用 try-catch 處理異常
ct.ThrowIfCancellationRequested();
// 執(zhí)行一些工作
await DoSomeWorkAsync(i, ct);
}
}工作原理:
- 調(diào)用
ThrowIfCancellationRequested()檢查 token 的狀態(tài) - 如果
IsCancellationRequested為true,拋出OperationCanceledException - 拋出異常后,后續(xù)代碼不會執(zhí)行,方法立即退出
- 異常會向上傳播,直到被某個 try-catch 捕獲
適用場景:
- 想要立即停止操作,不需要做任何清理工作
- 希望取消信號以異常的形式向上傳播
- 方法調(diào)用鏈中已經(jīng)有人處理
OperationCanceledException
方式2:IsCancellationRequested - 自行處理
async Task ProcessDataAsync(CancellationToken ct)
{
List<int> processedData = new List<int>();
for (int i = 0; i < 1000; i++)
{
// 方式2:檢查后自行處理
// 只檢查狀態(tài),不拋出異常
// 優(yōu)點:可以執(zhí)行清理邏輯,控制退出方式
// 缺點:需要自己編寫退出邏輯
if (ct.IsCancellationRequested)
{
Console.WriteLine("檢測到取消請求,開始清理資源...");
// 可以在這里執(zhí)行清理工作
// 例如:保存已處理的數(shù)據(jù)、關(guān)閉連接、釋放資源等
await SavePartialResultsAsync(processedData);
// 優(yōu)雅地退出方法
return; // 正常返回,不拋出異常
}
// 執(zhí)行工作
int result = await DoSomeWorkAsync(i, ct);
processedData.Add(result);
}
}工作原理:
- 檢查
IsCancellationRequested屬性 - 如果為
true,執(zhí)行自定義的清理邏輯 - 使用
return正常退出方法,不拋出異常 - 調(diào)用方不知道方法是被取消的,只認為方法正常完成
適用場景:
- 需要在取消時執(zhí)行特定的清理工作
- 希望部分完成的結(jié)果能夠被保存
- 不希望以異常的形式通知調(diào)用方
- 需要記錄取消事件
兩種方式的對比
| 特性 | ThrowIfCancellationRequested() | IsCancellationRequested |
|---|---|---|
| 是否拋出異常 | 是 | 否 |
| 代碼簡潔度 | 高 | 中等 |
| 能否執(zhí)行清理邏輯 | 只能在 finally 塊中 | 可以在檢測后立即執(zhí)行 |
| 調(diào)用方是否知道取消 | 是(通過異常) | 否(正常返回) |
| 推薦使用場景 | 大多數(shù)情況 | 需要特殊清理邏輯時 |
實際應(yīng)用示例
讓我們通過一個實際的運動控制例子來理解兩種方式的區(qū)別:
// 場景1:簡單的數(shù)據(jù)處理,不需要清理
async Task SimpleProcessAsync(CancellationToken ct)
{
for (int i = 0; i < 1000; i++)
{
ct.ThrowIfCancellationRequested(); // ? 使用 ThrowIfCancellationRequested
await ProcessItemAsync(i, ct);
}
}
// 場景2:需要保存中間結(jié)果
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:需要通知調(diào)用方取消狀態(tài)
async Task ProcessWithNotificationAsync(CancellationToken ct)
{
try
{
for (int i = 0; i < 1000; i++)
{
ct.ThrowIfCancellationRequested(); // ? 使用 ThrowIfCancellationRequested
await ProcessItemAsync(i, ct);
}
}
catch (OperationCanceledException)
{
// 重新拋出,讓調(diào)用方知道操作被取消
throw; // ? 拋出異常通知調(diào)用方
}
}
// 場景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 窗體應(yīng)用程序中使用 CancellationToken 是非常常見的場景。這通常涉及用戶界面(UI)和后臺任務(wù)的交互。讓我們通過一個完整的例子來學(xué)習(xí)如何正確實現(xiàn)。
public partial class MainForm : Form
{
// 字段:存儲 CancellationTokenSource 和當(dāng)前任務(wù)
private CancellationTokenSource _cts;
private Task _currentTask;
public MainForm()
{
InitializeComponent();
// 初始化時,取消按鈕應(yīng)該禁用
btnCancel.Enabled = false;
}
// 開始按鈕點擊事件
private async void btnStart_Click(object sender, EventArgs e)
{
// 步驟1:創(chuàng)建新的 CancellationTokenSource
// 每次開始新任務(wù)時,都應(yīng)該創(chuàng)建一個新的實例
_cts = new CancellationTokenSource();
// 步驟2:更新 UI 狀態(tài)
// 禁用開始按鈕,啟用取消按鈕,防止重復(fù)點擊
btnStart.Enabled = false;
btnCancel.Enabled = true;
progressBar1.Value = 0;
lblStatus.Text = "準備開始...";
try
{
// 步驟3:啟動長時間運行的操作
// 將 token 傳遞給方法,使其支持取消
_currentTask = LongRunningOperation(_cts.Token);
// 步驟4:等待操作完成
// 使用 await 關(guān)鍵字,不會阻塞 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 = "發(fā)生錯誤";
MessageBox.Show($"發(fā)生錯誤:{ex.Message}", "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
// 步驟8:清理資源,恢復(fù) UI 狀態(tài)
// finally 塊中的代碼總是會執(zhí)行,無論是否發(fā)生異常
btnStart.Enabled = true;
btnCancel.Enabled = false;
// 釋放 CancellationTokenSource
_cts?.Dispose();
_cts = null;
_currentTask = null;
}
}
// 取消按鈕點擊事件
private void btnCancel_Click(object sender, EventArgs e)
{
// 步驟1:發(fā)出取消信號
// 調(diào)用 Cancel() 會通知所有持有該 token 的任務(wù)
_cts?.Cancel();
// 步驟2:更新 UI
// 禁用取消按鈕,防止重復(fù)點擊
btnCancel.Enabled = false;
lblStatus.Text = "正在取消...";
}
// 長時間運行的操作
// 這個方法會在后臺線程執(zhí)行,不會阻塞 UI
private async Task LongRunningOperation(CancellationToken ct)
{
// 模擬處理 100 個項目
for (int i = 0; i < 100; i++)
{
// 步驟1:檢查是否請求取消
// 這是在后臺線程執(zhí)行的,不會阻塞 UI
ct.ThrowIfCancellationRequested();
// 步驟2:執(zhí)行一些工作
// 這里用 Task.Delay 模擬耗時操作
await Task.Delay(100, ct);
// 步驟3:更新 UI(重要?。?
// 由于這個方法在后臺線程運行,不能直接訪問 UI 控件
// 必須使用 Invoke 或 BeginInvoke 切換到 UI 線程
this.Invoke((MethodInvoker)delegate
{
// 更新進度條
progressBar1.Value = i + 1;
// 更新狀態(tài)標簽
lblStatus.Text = $"處理中... {i + 1}/100";
// 可以添加其他 UI 更新
lblProgress.Text = $"{i + 1}%";
});
}
}
}重要概念解釋:
1. 為什么需要字段變量?
// 使用字段而不是局部變量的原因:
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. 為什么每次都要創(chuàng)建新的 CancellationTokenSource?
// ? 錯誤:重復(fù)使用同一個實例
private CancellationTokenSource _cts = new CancellationTokenSource();
private async void btnStart_Click(object sender, EventArgs e)
{
// 如果用戶點擊兩次"開始",會出現(xiàn)問題
// 第一次點擊已經(jīng)開始,第二次點擊又創(chuàng)建任務(wù)
// 當(dāng)?shù)诙吸c擊后點擊"取消",兩次任務(wù)都會被取消
_cts.Cancel(); // 這會影響所有使用過這個 token 的任務(wù)
}
// ? 正確:每次開始新任務(wù)時創(chuàng)建新實例
private async void btnStart_Click(object sender, EventArgs e)
{
// 每次點擊"開始",都創(chuàng)建一個新的 CancellationTokenSource
// 這樣不同的任務(wù)之間互不影響
_cts = new CancellationTokenSource();
}3. 為什么要使用 Invoke 更新 UI?
在 WinForms 中,有一條重要規(guī)則:只有創(chuàng)建控件的線程(UI 線程)才能訪問該控件。
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 線程執(zhí)行
progressBar1.Value = i + 1;
lblStatus.Text = $"處理中... {i + 1}/100";
});
}
}Invoke 的工作原理:
Invoke會將代碼排隊到 UI 線程的消息隊列中- UI 線程在處理消息時執(zhí)行這段代碼
Invoke是同步的,會等待 UI 線程執(zhí)行完成- 還可以使用
BeginInvoke,它是異步的,不會等待
四、高級用法
4.1 注冊取消回調(diào)
有時候我們需要在取消發(fā)生時執(zhí)行一些特定的操作,比如清理資源、關(guān)閉設(shè)備連接、記錄日志等。CancellationToken 提供了 Register 方法,允許我們注冊回調(diào)函數(shù),當(dāng)取消發(fā)生時會自動執(zhí)行這些回調(diào)。
基本用法
// 創(chuàng)建 CancellationTokenSource
CancellationTokenSource cts = new CancellationTokenSource();
// 注冊取消時的回調(diào)
// Register 方法返回一個 CancellationTokenRegistration 對象
// 這個對象可以用于注銷回調(diào)
CancellationTokenRegistration registration = cts.Token.Register(() =>
{
Console.WriteLine("執(zhí)行清理工作...");
// 在這里執(zhí)行清理操作
// 例如:關(guān)閉文件、釋放資源、關(guān)閉設(shè)備連接等
CleanupResources();
// 例如:記錄日志
LogCancellation();
});
// 執(zhí)行一些工作
await DoSomeWorkAsync(cts.Token);
// 觸發(fā)取消
cts.Cancel();
// 手動注銷回調(diào)(如果需要)
// 如果回調(diào)已經(jīng)執(zhí)行,注銷沒有影響
registration.Dispose();詳細說明:
1. Register 的工作原理
// Register 方法的簽名 // public CancellationTokenRegistration Register(Action callback) // 回調(diào)函數(shù)會在以下情況被執(zhí)行: // 1. 調(diào)用 CancellationTokenSource.Cancel() // 2. 調(diào)用 CancellationTokenSource.CancelAfter() 超時后自動取消 // 3. 關(guān)聯(lián)的令牌源被取消(使用 CreateLinkedTokenSource 創(chuàng)建的) // 回調(diào)函數(shù)的執(zhí)行時機: // - 在 Cancel() 調(diào)用的線程中同步執(zhí)行 // - 如果注冊了多個回調(diào),會按照注冊順序依次執(zhí)行 // - 回調(diào)函數(shù)執(zhí)行期間,其他線程可能繼續(xù)運行
2. 注冊多個回調(diào)
CancellationTokenSource cts = new CancellationTokenSource();
// 可以注冊多個回調(diào)
var registration1 = cts.Token.Register(() =>
{
Console.WriteLine("回調(diào)1: 關(guān)閉數(shù)據(jù)庫連接");
CloseDatabaseConnection();
});
var registration2 = cts.Token.Register(() =>
{
Console.WriteLine("回調(diào)2: 清理臨時文件");
CleanTempFiles();
});
var registration3 = cts.Token.Register(() =>
{
Console.WriteLine("回調(diào)3: 記錄取消日志");
LogCancellation();
});
// 當(dāng)調(diào)用 Cancel() 時,所有回調(diào)都會執(zhí)行
cts.Cancel();
// 輸出:
// 回調(diào)1: 關(guān)閉數(shù)據(jù)庫連接
// 回調(diào)2: 清理臨時文件
// 回調(diào)3: 記錄取消日志
// 清理注冊對象
registration1.Dispose();
registration2.Dispose();
registration3.Dispose();3. 回調(diào)的執(zhí)行順序
CancellationTokenSource cts = new CancellationTokenSource();
// 注冊回調(diào)的順序就是執(zhí)行順序
cts.Token.Register(() => Console.WriteLine("第1個回調(diào)"));
cts.Token.Register(() => Console.WriteLine("第2個回調(diào)"));
cts.Token.Register(() => Console.WriteLine("第3個回調(diào)"));
cts.Cancel();
// 輸出:
// 第1個回調(diào)
// 第2個回調(diào)
// 第3個回調(diào)4. 回調(diào)中拋出異常
CancellationTokenSource cts = new CancellationTokenSource();
// 如果回調(diào)中拋出異常,會阻止后續(xù)回調(diào)執(zhí)行
cts.Token.Register(() =>
{
Console.WriteLine("回調(diào)1開始");
throw new Exception("回調(diào)1出錯"); // 拋出異常
});
cts.Token.Register(() =>
{
Console.WriteLine("回調(diào)2"); // 不會執(zhí)行!
});
try
{
cts.Cancel();
}
catch (AggregateException ex)
{
Console.WriteLine($"捕獲到異常: {ex.InnerExceptions[0].Message}");
}
// 輸出:
// 回調(diào)1開始
// 捕獲到異常: 回調(diào)1出錯可以在回調(diào)中直接catch到異常,這樣即使出現(xiàn)了異常,也不會阻止后面的回調(diào)
Register 方法的關(guān)鍵要點:
- 回調(diào)是同步執(zhí)行的:回調(diào)會在調(diào)用
Cancel()的線程中同步執(zhí)行 - 可以注冊多個回調(diào):按照注冊順序依次執(zhí)行
- 應(yīng)該捕獲異常:回調(diào)中應(yīng)該捕獲異常,避免影響其他回調(diào)
- 可以注銷回調(diào):通過返回的
CancellationTokenRegistration對象可以注銷回調(diào) 適合用于資源清理:在取消時自動清理資源,避免資源泄漏
4.2 傳遞 CancellationToken 到多個異步操作
在實際應(yīng)用中,我們經(jīng)常需要并行執(zhí)行多個任務(wù),并且希望能夠一次性取消所有任務(wù)。通過將同一個 CancellationToken 傳遞給所有任務(wù),可以輕松實現(xiàn)這個需求。
async Task ComplexOperationAsync(CancellationToken ct)
{
var tasks = new List<Task>();
// 并行執(zhí)行多個任務(wù),共享同一個取消令牌
// 這樣,當(dāng)令牌被取消時,所有任務(wù)都會收到通知
tasks.Add(Task.Run(() => Task1(ct), ct));
tasks.Add(Task.Run(() => Task2(ct), ct));
tasks.Add(Task.Run(() => Task3(ct), ct));
try
{
// 等待所有任務(wù)完成
await Task.WhenAll(tasks);
Console.WriteLine("所有任務(wù)完成");
}
catch (OperationCanceledException)
{
Console.WriteLine("某個或所有任務(wù)被取消");
throw;
}
catch (AggregateException ex)
{
// 捕獲多個任務(wù)的異常
Console.WriteLine($"捕獲到 {ex.InnerExceptions.Count} 個異常");
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($" - {innerEx.Message}");
}
throw;
}
}
// 各個任務(wù)的實現(xiàn)
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 完成");
}使用示例:
var cts = new CancellationTokenSource();
// 啟動復(fù)雜操作
var operationTask = ComplexOperationAsync(cts.Token);
// 等待3秒后取消
await Task.Delay(3000);
Console.WriteLine("取消所有任務(wù)...");
cts.Cancel();
try
{
await operationTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("操作已取消");
}輸出示例:
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 取消所有任務(wù)... 某個或所有任務(wù)被取消 操作已取消
4.3 超時控制
超時控制是 CancellationToken 的一個重要應(yīng)用場景。在運動控制系統(tǒng)中,我們經(jīng)常需要設(shè)置超時,防止某個操作長時間卡死。
方式1:使用 CancellationTokenSource 的構(gòu)造函數(shù)
// 方式1:使用帶超時的構(gòu)造函數(shù)
// 創(chuàng)建一個會在指定時間后自動取消的 CancellationTokenSource
async Task<T> WithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout)
{
// 創(chuàng)建一個會在 timeout 時間后自動取消的 CancellationTokenSource
using var cts = new CancellationTokenSource(timeout);
try
{
// 等待任務(wù)完成
return await task;
}
catch (OperationCanceledException) when (!cts.Token.IsCancellationRequested)
{
// 任務(wù)自己取消了,不是超時
// 這種情況下,異常是由任務(wù)本身拋出的,而不是超時
throw;
}
catch (OperationCanceledException)
{
// cts.Token.IsCancellationRequested 為 true
// 說明是超時導(dǎo)致的取消
throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
}
}方式2:使用 Task.WhenAny 實現(xiàn)超時
// 方式2:使用 Task.WhenAny 比較原任務(wù)和超時任務(wù)
async Task<T> WithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout)
{
// 創(chuàng)建超時任務(wù)
var timeoutTask = Task.Delay(timeout);
// 等待任一任務(wù)完成
var completedTask = await Task.WhenAny(task, timeoutTask);
// 如果超時任務(wù)先完成,拋出超時異常
if (completedTask == timeoutTask)
{
throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
}
// 否則返回原任務(wù)的結(jié)果
return await task;
}方式3:結(jié)合 CancellationToken 和 Task.WhenAny
// 方式3:結(jié)合使用 CancellationToken 和 Task.WhenAny
// 這樣可以真正取消原任務(wù),而不只是超時
async Task<T> WithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout)
{
// 創(chuàng)建超時的 CancellationTokenSource
using var cts = new CancellationTokenSource(timeout);
try
{
// 使用 Task.WhenAny 等待任一任務(wù)完成
var completedTask = await Task.WhenAny(task, Task.Delay(timeout, cts.Token));
// 如果原任務(wù)先完成,返回結(jié)果
if (completedTask == task)
{
return await task;
}
// 如果超時,拋出異常
throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
}
catch (OperationCanceledException) when (cts.Token.IsCancellationRequested)
{
// 確認是超時導(dǎo)致的取消
throw new TimeoutException($"操作超時({timeout.TotalSeconds}秒)");
}
}以上就是C#使用Cancellation取消異步任務(wù)的全過程的詳細內(nèi)容,更多關(guān)于C# Cancellation取消異步任務(wù)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解C#如何使用屏障實現(xiàn)多線程并發(fā)操作保持同步
這篇文章主要為大家詳細介紹了C#如何使用屏障實現(xiàn)多線程并發(fā)操作保持同步,文中的示例代碼簡潔易懂,具有一定的借鑒價值,有需要的小伙伴可以參考下2024-01-01
C#調(diào)用OutLokk實現(xiàn)發(fā)送郵件
這篇文章主要為大家詳細介紹了如何利用C#調(diào)用OutLokk實現(xiàn)發(fā)送郵件的功能,文中的示例代碼講解詳細,對我們學(xué)習(xí)C#有一定的幫助,感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
VsCode使用EmmyLua插件調(diào)試Unity工程Lua代碼的詳細步驟
這篇文章主要介紹了VsCode使用EmmyLua插件調(diào)試Unity工程Lua代碼,本文通過圖文并茂的形式給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-08-08
C#實現(xiàn)將Email地址轉(zhuǎn)成圖片顯示的方法
這篇文章主要介紹了C#實現(xiàn)將Email地址轉(zhuǎn)成圖片顯示的方法,涉及C#操作圖片的相關(guān)技巧,需要的朋友可以參考下2015-06-06

