C#

[C#] CancellationToken 一直都有寫,但專案其實從來沒有真的取消過

本文探討在 .NET 專案進行非同步化重構時,開發者常在 CancellationToken 上犯的錯誤,包括忽略傳遞 Token、使用自定義旗標取代、錯誤的異常處理以及誤用 Task.Run 等,並提供正確的程式碼範例。

[C#] CancellationToken 一直都有寫,但專案其實從來沒有真的取消過

最近都再釐清一些觀念跟償還技術債,其實只要常常遇到程式要大量改寫成非同步化常常對專案都是

毀滅性的更改,也就是大量翻修,這裡面有一個很常被忽略的東西,處理的好才能夠把非同步化達到最大價值

這邊我列出幾點,我這邊常遇到的狀況也附上我錯誤的寫法,也提醒自己不要之後不要犯這些錯

1. 方法有 CancellationToken,但實際完全沒用

    
    public async Task DoWorkAsync(CancellationToken ct)
    {
        // 錯誤:ct 傳進來了,但完全沒用
        // await Task.Delay(5000);

        // 改成把 ct 傳進支援取消的 API
        await Task.Delay(5000, ct);
    }

    // 呼叫方式
    using var cts = new CancellationTokenSource();
    var task = DoWorkAsync(cts.Token);

    await Task.Delay(1000);
    cts.Cancel();

    await task;

    
  

2. 用 bool 或旗標自己做取消,這我很常用真的是壞習慣

    
    
    public async Task DoWorkAsync(CancellationToken ct)
    {
        // 寫法:自己做取消旗標,Framework 完全不知情
        // while (!_cancel)
        // {
        //     await Task.Delay(100);
        // }

        // 改成用 CancellationToken 控制流程
        while (!ct.IsCancellationRequested)
        {
            await Task.Delay(100, ct);
        }
    }

    // 呼叫方式
    using var cts = new CancellationTokenSource();
    var task = DoWorkAsync(cts.Token);

    cts.Cancel();
    await task;
    
    
  

3. 不用 OperationCanceledException,而是用大絕招的 catch all

    
    
    public async Task DoWorkAsync(CancellationToken ct)
    {
        try
        {
            await Task.Delay(5000, ct);
        }
        // 錯誤:把取消吃掉,上層完全不知道
        // catch
        // {
        // }

        // 改成允許取消正常往上傳
        catch (OperationCanceledException)
        {
            throw;
        }
    }

    // 呼叫方式
    try
    {
        await DoWorkAsync(ct);
    }
    catch (OperationCanceledException)
    {
        // 明確知道這是取消,不是錯誤
    }

    
    
  

4. 用 Task.Run 包同步程式碼,明明底層就有提供非同步的作法

    
    public async Task DoWorkAsync(CancellationToken ct)
    {
        // 錯誤:CancellationToken 無法中斷同步程式碼
        // await Task.Run(() =>
        // {
        //     Thread.Sleep(5000);
        // }, ct);

        //改成流程本身就是可取消的非同步 API
        await Task.Delay(5000, ct);
    }

    // 呼叫方式
    using var cts = new CancellationTokenSource();
    var task = DoWorkAsync(cts.Token);

    cts.Cancel();
    await task;

    
    
  

5. 不使用底層內建的取消直接無腦忽略,只為了編譯過

    
    
    public async Task Get(CancellationToken ct)
    {
        // 錯誤:直接忽略 RequestAborted
        // await DoWorkAsync(CancellationToken.None);

        // 改成使用 ASP.NET Core 提供的取消來源
        await DoWorkAsync(ct);
        return Ok();
    }

    // 呼叫方式
    // Client 關閉頁面就中斷連線觸發 ct

    
    
  

來個小結論:

在 .NET 裡,CancellationToken 常常不是忽略就會為了方便編譯過就亂寫

只有當它被實際用在支援取消的 API 上、沒有被自行取代或自動執行

系統行為上才真的存在取消這件事,否則不論程式碼看起來多完整,結果都等同於沒有取消。

這邊也是提醒自己不要再亂處理 CancellationToken 除了寫範例以外 :P