CodeGym /課程 /C# SELF /取消操作: CancellationToken

取消操作: CancellationToken

C# SELF
等級 60 , 課堂 4
開放

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.GetAsyncStream.ReadAsyncTask.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.ReadAsyncHttpClient.GetAsyncTask.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
await Task.Delay(1000, token);
HttpClient.GetAsync
await client.GetAsync(url, token);
Stream.ReadAsync
await stream.ReadAsync(buffer, 0, len, token);
Task.Run
Task.Run(() => ..., token);
File.ReadAllTextAsync
await File.ReadAllTextAsync(path, token);
Thread.Sleep
不支援;建議改用 Task.Delay
你的方法 ✔(如果你加入支援!)
MyLongMethod(token)

可取消操作的生命週期

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 已經被取消,方法會立刻拋出例外,若沒有處理會破壞程式流程。

1
問卷/小測驗
Task 跟 Thread 的比較,等級 60,課堂 4
未開放
Task 跟 Thread 的比較
資料的平行處理
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION