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

  • 157
  • 0

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

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

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

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

--

本文原文首發於個人部落格:CancellationToken 一直都有寫,但專案其實從來沒有真的取消過

--

---

The bug existed in all possible states.
Until I ran the code.