最近都再釐清一些觀念跟償還技術債,其實只要常常遇到程式要大量改寫成非同步化常常對專案都是
毀滅性的更改,也就是大量翻修,這裡面有一個很常被忽略的東西,處理的好才能夠把非同步化達到最大價值
這邊我列出幾點,我這邊常遇到的狀況也附上我錯誤的寫法,也提醒自己不要之後不要犯這些錯
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.