1. 認識 CancellationToken
想像這種情況:你開始下載一個大檔案,然後突然想到你的網路是計量的、每 GB 都很寶貴。或者使用者搞錯了,啟動了一個計算,然後決定不需要了。他想按下「取消」,不用等到結束。現代應用應該是有回應性的,為此我們需要能在任何時刻傳遞停止信號並正確中斷非同步操作。
這就是 .NET 裡的取消機制存在的原因 —— CancellationToken。
CancellationToken(字面是「取消的 token」)是一個你傳入長時間運行操作的特殊物件。你可以透過 CancellationTokenSource 在任何時候發出取消信號,操作則應該定期檢查 token(例如 IsCancellationRequested)並在必要時呼叫 ThrowIfCancellationRequested() 來正確結束。
它怎麼組成的?(簡短)
- 有個物件 CancellationTokenSource,負責「產生」token 並能取消它們(方法 Cancel())。
- CancellationToken 本身是一個「信號旗」,可以分發給多個操作(透過來源的 Token 屬性)。
- 操作會定期檢查 token:如果要求取消,就結束工作(或拋出 OperationCanceledException)。
類比:你是老闆(你就是 CancellationTokenSource)。你發給員工「識別證」(就是 CancellationToken)。當你決定要全部取消時,你舉紅旗——有識別證的人就會立刻撤退,連沒吃完的午餐也丟下一半。
2. 如何使用 CancellationToken
建立取消來源 (CancellationTokenSource)
var cts = new CancellationTokenSource();
取得 token 本身 (CancellationToken)
CancellationToken token = cts.Token;
把 token 傳給非同步方法
大多數標準的 .NET 非同步方法都接受一個 CancellationToken 參數。例如 HttpClient.GetAsync、Stream.ReadAsync、Task.Delay 等等。
範例 — 支援取消的延遲:
await Task.Delay(10000, token); // 等 10 秒 — 但可以取消!
發出取消(例如按鈕或計時器)
cts.Cancel(); // 所有拿到這個 token 的操作都會收到取消通知
在方法內檢查 token
在你的方法內(特別是長時間或循環工作的情況)需要定期檢查取消旗標,若有取消請拋出 OperationCanceledException:
token.ThrowIfCancellationRequested();
或者簡單檢查屬性:
if (token.IsCancellationRequested)
{
// 釋放資源,離開方法
}
3. 範例:在教學應用加入取消功能
假設我們有個從網站下載資料的應用。現在加入讓使用者在改變主意時可以取消下載的功能。
基本的非同步下載範例
using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
class Downloader
{
public async Task DownloadAsync(string url)
{
var client = new HttpClient();
string content = await client.GetStringAsync(url); // 沒有取消
Console.WriteLine("下載完成!");
}
}
加入 CancellationToken
public async Task DownloadAsync(string url, CancellationToken token)
{
var client = new HttpClient();
string content = await client.GetStringAsync(url, token); // 現在支援取消了!
Console.WriteLine("下載完成!");
}
由呼叫端控制取消
static async Task Main(string[] args)
{
var downloader = new Downloader();
var cts = new CancellationTokenSource();
Console.WriteLine("輸入要下載的 URL:");
string url = Console.ReadLine();
var downloadTask = downloader.DownloadAsync(url, cts.Token);
Console.WriteLine("按任意鍵取消下載...");
Console.ReadKey();
cts.Cancel(); // 發出取消信號
try
{
await downloadTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("下載已被使用者取消!");
}
}
就這麼簡單!現在使用者可以在任何時刻中止操作。
4. CancellationTokenSource 與方法間的互動
flowchart TD
A["使用者程式碼 (Main)"] -- 創建 --> B["CancellationTokenSource"]
B -- 發出 --> C["CancellationToken"]
C -- 傳遞到 --> D["非同步操作"]
A -- 調用 Cancel() --> B
D -- 定期檢查 --> C
C -- 通知取消 --> D
D -- 拋出 Exception 或 結束工作 --> A
5. 處理取消
當操作拿到取消 token,有兩種行為模式:
.NET 的方法會自行拋出例外。
如果你呼叫標準方法,例如 Stream.ReadAsync、HttpClient.GetAsync 或 Task.Delay,並傳入 token,當 Cancel() 被呼叫時,這些方法會自動拋出 OperationCanceledException。你只需要捕捉這個例外。
自訂的非同步程式碼。
如果你自己實作一個長時間運作的程序,例如循環處理或複雜計算,責任在於你要定期檢查 token.IsCancellationRequested(或呼叫 token.ThrowIfCancellationRequested()),以便正確回應取消。
範例:手動檢查取消的「長時間」操作
public async Task CalculatePrimesAsync(int max, CancellationToken token)
{
for (int i = 2; i < max; i++)
{
token.ThrowIfCancellationRequested(); // 檢查取消
if (IsPrime(i))
{
Console.WriteLine($"質數: {i}");
await Task.Delay(100, token); // 讓出「喘息」時間(可以取消)
}
}
Console.WriteLine("計算完成!");
}
private bool IsPrime(int n)
{
for (int i = 2; i <= Math.Sqrt(n); i++)
if (n % i == 0) return false;
return true;
}
6. 有用的細節
支援 CancellationToken 的方法與類別
| 類別/方法 | 支援 CancellationToken? | 使用範例 |
|---|---|---|
|
✔ | |
|
✔ | |
|
✔ | |
|
✔ | |
|
✔ | |
|
✖ | 不支援;建議改用 Task.Delay |
| 你的方法 | ✔(如果你加入支援!) | |
可取消操作的生命週期
sequenceDiagram
participant 使用者
participant Main
participant CancellationTokenSource
participant 非同步操作
使用者->>Main: 啟動操作
Main->>CancellationTokenSource: 建立 CTS
Main->>非同步操作: 啟動並傳入 CancellationToken
使用者->>Main: 按下 "取消"
Main->>CancellationTokenSource: 呼叫 Cancel()
非同步操作->>非同步操作: 偵測到取消 (\nIsCancellationRequested)
非同步操作-->>Main: 拋出 OperationCanceledException
Main->>使用者: 顯示訊息 "操作已取消"
真實世界的使用場景
- UI 應用:中斷長時間下載、計算、檔案操作,若使用者想關閉視窗或取消動作。
- 伺服器應用:若客戶端中斷連線——最好立刻取消請求處理,避免浪費資源。
- 大資料處理:任務可能非常長,應提供停止計算或遷移的能力。
- 與設備整合:掃描、列印等操作有時需緊急中止——這裡一定要支援取消。
7. 使用 CancellationToken 時常見錯誤
錯誤 №1:忽略檢查 token。
如果操作不檢查 token.IsCancellationRequested 或不呼叫 ThrowIfCancellationRequested(),在取消時它不會停止,會繼續浪費資源。
錯誤 №2:錯誤處理 OperationCanceledException。
如果不捕捉 OperationCanceledException,應用可能會異常終止。務必使用 try-catch 來處理取消。
錯誤 №3:取消時資源管理不當。
取消不會自動回滾變更(例如檔案或資料庫)。你需要在 catch 裡手動清理資源。
錯誤 №4:傳遞已被取消的 token。
如果 token 已經被取消,方法會立刻拋出例外,若沒有處理會破壞程式流程。
GO TO FULL VERSION