C# CancellationToken 取消令牌 介紹

這是一個很簡單的非同步程式碼(.NET 8, C# 12):
    
await Task.Run(async () =>
{
    Console.WriteLine("Step 1");
    await Task.Delay(1000);
    Console.WriteLine("Step 2");
});
    

可以使用 CancellationTokenSource ,在 Task.Run 執行前如果使用 cancellationTokenSource.Cancel() 方法標記取消,Task.Run 就會不會執行,但是會拋出 TaskCanceledException 例外:
    
CancellationTokenSource cancellationTokenSource = new();

cancellationTokenSource.Cancel(); // 取消任務

try
{
    await Task.Run(async () =>
    {
        Console.WriteLine("步驟 1");
        await Task.Delay(1000); // 假裝是耗時的任務
        Console.WriteLine("步驟 2");
    }, cancellationTokenSource.Token);
}
catch (System.Threading.Tasks.TaskCanceledException)
{
    Console.WriteLine("任務已取消");
}
    

CancellationToken 被包裝在 CancellationTokenSource 內,需要透過 CancellationTokenSource 來使用。CancellationTokenSource.Token 就是 CancellationToken

也可以註冊取消時會觸發的事件:
    
CancellationTokenSource cancellationTokenSource = new();
cancellationTokenSource.Token.Register(() => Console.WriteLine("已取消"));
    

剛剛是任務已經取消,才輪到 Task.Run 執行,因為在傳入的參數發現任務已取消,所以不執行任務,直接拋出例外。
那如果是任務執行到一半發現要取消可以怎麼做?

假設有任務按步驟順序是 1, 2-1, 2-2, 3 ,在步驟 1 到 2-1 中間和 2-2 到 3 之間如果要取消任務則可以直接終止任務,那可以使用手動判斷的方式:
    
CancellationTokenSource cancellationTokenSource = new();

// 500 毫秒後取消任務
Task.Delay(500).ContinueWith(_ => cancellationTokenSource.Cancel());

try
{
    await Task.Run(async () =>
    {
        Console.WriteLine("步驟 1");
        await Task.Delay(1000); // 假裝是耗時的任務

        if (cancellationTokenSource.Token.IsCancellationRequested)
            return;

        Console.WriteLine("步驟 2-1");
        await Task.Delay(1000);
        Console.WriteLine("步驟 2-2");
        await Task.Delay(1000);

        if (cancellationTokenSource.Token.IsCancellationRequested)
            return;

        Console.WriteLine("步驟 3");
    }, cancellationTokenSource.Token);
}
catch (TaskCanceledException e)
{
    Console.WriteLine(e);
    Console.WriteLine("任務已取消");
}
    

那 Task.Delay 到一半可以終止嗎?
上面的 Task.Delay 都是用來模擬需要耗費一段時間的任務,但是假設在程式碼中真的會使用到 Task.Delay ,可以在 Task.Delay 中傳入 cancellationTokenSource.Token 參數,如果中途任務被取消,就不會等到任務結束,而是會直接拋出 TaskCanceledException 例外,省去了剩餘的等待時間
    
CancellationTokenSource cancellationTokenSource = new();
await Task.Delay(1000, cancellationTokenSource.Token);
    

在日常使用中假設有一個自訂的 MyTask:
    
async Task MyTask(CancellationToken cancellationToken)
{
    Console.WriteLine("步驟 1");
    await Task.Delay(1000); // 假裝是耗時的任務

    if (cancellationToken.IsCancellationRequested)
        return;

    Console.WriteLine("步驟 2-1");
    Console.WriteLine("步驟 2-2");
    await Task.Delay(1000);

    if (cancellationToken.IsCancellationRequested)
        return;

    Console.WriteLine("步驟 3");
}
    

我們可以這樣使用:
    
        CancellationTokenSource cancellationTokenSource = new();
        await MyTask(cancellationTokenSource.Token);
    

假設確定絕對不會中途取消,也不需要先建立 CancellationTokenSource ,可以直接這樣:
    
await MyTask(CancellationToken.None);
    



參考資料:
Microsoft.Learn - CancellationToken Struct
Microsoft.Learn - CancellationTokenSource Class
Microsoft.Learn - OperationCanceledException Class
Microsoft.Learn - Cancellation in Managed Threads

留言

張貼留言

如果有任何問題、建議、想說的話或文章題目推薦,都歡迎留言或來信: a@ruyut.com