C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表

· · C#, async/await, .NET, 設計

C# 的 async / await 日常使用,但實務上容易迷惘的比起語法本身,是 在哪個場面該選哪個寫法。 特別是搜尋中多見的是 Task.Run 何時使用、ConfigureAwait(false) 放在哪、fire-and-forget 是否可接受等判斷上的煩惱。

  • 明明是 I/O 等待卻用 Task.Run 包起來
  • 明明是獨立的處理卻一件一件直列地 await
  • 輕率放入 fire-and-forget 失去例外或終止的時機
  • 到處一樣地加 ConfigureAwait(false)
  • 僅因「看起來輕」就選 ValueTask

這一帶比起個別記憶,先區別處理種類 比較容易整理。

本文以 .NET 6 以後的一般 C# / .NET 應用程式開發 為前提,以 容易判斷的順序 整理 async / await 周邊的寫法。

對象例如以下。

  • WinForms / WPF 等的桌面應用程式
  • ASP.NET Core 的 Web 應用程式 / API
  • worker / 背景服務
  • 主控台應用程式
  • 可再利用的類別函式庫

1. 先講結論(一句話)

  • async / await等待中不佔用執行緒的寫法,不是自動加速或隨意別執行緒化的機制
  • 先區分該處理是 I/O 等待 還是 CPU 計算
  • 如果是 I/O 等待,基本是 直接 await async API
  • 如果是 CPU 計算,思考 該在哪執行該計算。UI 有時 Task.Run 有用,但 ASP.NET Core 的請求處理基本避免寫 Task.Run 後立即 await
  • 獨立的多個處理比起直列 await,先檢討 Task.WhenAll
  • 件數多時,不要用 Task.WhenAll 全部同時丟,決定並行數的上限
  • fire-and-forget 看似簡單但難管理。真要與呼叫端分離壽命,放到 Channel 或 HostedService 等受管理位置 比較穩定
  • 回傳值 先用 Task / Task<T>ValueTask 計測後看到需求再選
  • ConfigureAwait(false)通用函式庫程式碼 很有力,UI 或應用程式端程式碼先用一般 await 即可
  • async void 除了事件處理器不使用

簡言之,async / await 周邊最重要的是 不要「總之 Task.Run」「總之 fire-and-forget」「總之 ValueTask」

先看,

  1. 該處理在等什麼
  2. 誰擁有該處理的壽命
  3. 在哪控制同時執行數

這 3 個就會相當容易整理。

2. 本文使用的用語

2.1. 先區別的用語

先分開這 2 個會相當不易混亂。

用語 這裡的意義
I/O-bound HTTP、DB、檔案、socket 等 等待外部完成 為中心的處理
CPU-bound 壓縮、影像處理、雜湊計算、沉重的變換等 CPU 計算本身 為中心的處理

async / await 特別有效的是 I/O 等待。 因為等待中可以把執行緒交給其他工作。

另一方面,CPU 計算不是「等待」而是「實際在計算的時間」。 這裡以 在哪個執行緒執行如何決定並行數 為主題。

2.2. 常出現的用語

用語 這裡的意義
阻塞 等待完成期間持續佔用該執行緒
fire-and-forget 呼叫端不等待完成的啟動方式
SynchronizationContext UI 等「回到原本執行位置」的機制
backpressure 流入太快時讓寫入端等待以防過度增加的機制

特別重要的是 非同步和並行是不同的東西

  • 非同步: 等待方式的話題
  • 並行: 同時推進的話題

這 2 個混淆會到處想用 Task.Run。 這裡是最初的分歧點。

3. 先看的判斷表

3.1. 整體

先看此表,大致方針會決定。

狀況 先用的 看的重點
HTTP / DB / 檔案等的等待 直接 await async API 不要用 Task.Run
不想停 UI 的沉重計算 Task.Run 把 CPU 計算從 UI 執行緒移出
ASP.NET Core 的請求處理 plain await 不要立即 await Task.Run
獨立的少數非同步處理 Task.WhenAll 先全部啟動後一起等
只要最先結束的 Task.WhenAny 考慮其餘的取消或例外回收
件數多想上限 Parallel.ForEachAsync / SemaphoreSlim 明確並行數
想依序流背景處理 Channel<T> 考慮有界佇列與 backpressure
一定間隔的非同步處理 PeriodicTimer 守 1 timer 1 consumer
想一件一件處理結果 IAsyncEnumerable<T> / await foreach 不等全件完成就推進
需要非同步釋放 await using IAsyncDisposable
跨 await 的互斥 SemaphoreSlim.WaitAsync try/finally 中一定 Release
通用函式庫程式碼 檢討 ConfigureAwait(false) 不依賴 UI / 應用程式特有 context
flowchart TD
    start["想做的處理"] --> q1{"等外部 I/O?"}
    q1 -- "是" --> p1["直接 await async API"]
    q1 -- "否" --> q2{"CPU 計算重?"}
    q2 -- "是" --> q3{"在哪執行?"}
    q3 -- "UI 事件 / 桌面" --> p2["考慮 Task.Run"]
    q3 -- "ASP.NET Core 請求" --> p3["不要用 Task.Run 包<br/>必要時移到別 worker 或佇列"]
    q3 -- "worker / 背景" --> p4["當場執行<br/>或明確並行度"]
    q2 -- "否" --> q4{"處理多件工作?"}
    q4 -- "等全部完成" --> p5["Task.WhenAll"]
    q4 -- "用最先結束的" --> p6["Task.WhenAny"]
    q4 -- "件數多" --> p7["Parallel.ForEachAsync<br/>或 SemaphoreSlim"]
    q4 -- "依序流" --> p8["Channel&lt;T&gt;"]
    q4 -- "一定間隔" --> p9["PeriodicTimer"]
    q4 -- "逐次 stream" --> p10["IAsyncEnumerable&lt;T&gt;"]

以下依序看各模式。

3.2. 如果是 I/O 等待就直接 await async API

最基本的模式。

例如 HTTP、DB、檔案讀寫等,先看 有沒有 async 版 API。 有的話直接 await 是基本。

public async Task<string> LoadTextAsync(string path, CancellationToken cancellationToken)
{
    return await File.ReadAllTextAsync(path, cancellationToken);
}

這時想避免的是把已經 async 的 I/O 用 Task.Run 包。

// 不好的例子
public async Task<string> LoadTextAsync(string path, CancellationToken cancellationToken)
{
    return await Task.Run(() => File.ReadAllTextAsync(path, cancellationToken), cancellationToken);
}

這只是把 I/O 等待丟到別執行緒,難整理又沒好處。

  • I/O 等待不需要 Task.Run
  • 先找 async API
  • 收到 token 就直接往下流傳

這裡相當王道。

3.3. CPU 負荷重時選擇 Task.Run 的位置

Task.Run 有效的是 想把 CPU 計算從當前執行緒移出時

例如在 UI 事件處理器中直接跑沉重計算會讓畫面停。 這時 Task.Run 自然。

public Task<byte[]> HashManyTimesAsync(byte[] data, int repeat, CancellationToken cancellationToken)
{
    return Task.Run(() =>
    {
        cancellationToken.ThrowIfCancellationRequested();

        using var sha256 = System.Security.Cryptography.SHA256.Create();
        byte[] current = data;

        for (int i = 0; i < repeat; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            current = sha256.ComputeHash(current);
        }

        return current;
    }, cancellationToken);
}

但這裡重要的是 在哪裡呼叫

  • WinForms / WPF 等的 UI: 有 Task.Run 有效的場面
  • ASP.NET Core 的請求處理: 基本避免立即 await Task.Run
  • worker / 背景處理: 當場處理或設計並行度

ASP.NET Core 的請求處理本來就在 ThreadPool 運作。 在那裡夾 1 層 Task.Run 後立即 await,只增加多餘的排程。

所以在 ASP.NET Core 以下思考比較容易整理。

  • I/O 等待用 plain await
  • 短的 CPU 處理當場執行
  • 長的處理或想脫離請求壽命的處理放到佇列或 HostedService

另外,只有 sync 版 API 從 UI 呼叫的情況,為 UI 回應性使用 Task.Run 可能有。 但這不是「非同步 I/O」,只是 佔用 1 個執行緒來迴避。 在 ASP.NET Core 這類伺服器側這種逃避方式基本不易擴展。

3.4. 獨立的多個處理就用 Task.WhenAll

明明有獨立的多個非同步處理,這種一件一件等的程式碼常見。

// 獨立卻直列的例子
string a = await _httpClient.GetStringAsync(urlA, cancellationToken);
string b = await _httpClient.GetStringAsync(urlB, cancellationToken);
string c = await _httpClient.GetStringAsync(urlC, cancellationToken);

這些互不依賴的話,先全部啟動,最後一起等 比較自然。

public async Task<string[]> DownloadAllAsync(IEnumerable<string> urls, CancellationToken cancellationToken)
{
    Task<string>[] tasks = urls
        .Select(url => _httpClient.GetStringAsync(url, cancellationToken))
        .ToArray();

    return await Task.WhenAll(tasks);
}

重點是 ToArray()。 LINQ 是 延遲執行,光 Select 可能尚未列舉。 用 ToArray()ToList() 先確定,那時所有 task 會開始。

sequenceDiagram
    participant Caller as 呼叫端
    participant T1 as Task 1
    participant T2 as Task 2
    participant T3 as Task 3

    Caller->>T1: 開始
    Caller->>T2: 開始
    Caller->>T3: 開始
    Caller->>Caller: await Task.WhenAll(...)
    T1-->>Caller: 完成
    T2-->>Caller: 完成
    T3-->>Caller: 完成

這個模式適合的是,

  • 件數少或中等
  • 想一次等全部
  • 無上限同時執行沒問題

這情況。

件數多的話如後面的 3.6 那樣 加上並行數上限 比較安全。

3.5. 用最先結束的就用 Task.WhenAny

例如想用多個鏡像中最先回應的情境,Task.WhenAny 易懂。

public async Task<byte[]> DownloadFromFirstMirrorAsync(
    IReadOnlyList<string> urls,
    CancellationToken cancellationToken)
{
    using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);

    Task<byte[]>[] tasks = urls
        .Select(url => _httpClient.GetByteArrayAsync(url, cts.Token))
        .ToArray();

    Task<byte[]> winner = await Task.WhenAny(tasks);
    cts.Cancel();

    try
    {
        return await winner;
    }
    finally
    {
        try
        {
            await Task.WhenAll(tasks);
        }
        catch
        {
            // 回收非贏家的取消或失敗
        }
    }
}

這裡要注意 WhenAny 只回傳 1 個贏家。 其餘的處理不做什麼就持續執行。

所以要先決定,

  • 其餘想取消嗎
  • 例外想觀測嗎

Task.WhenAny 雖方便但比 WhenAll 多一些設計。 「只要最先的 1 個就好」時選才易懂。

3.6. 件數多想限制並行數的話用 Parallel.ForEachAsync 或 SemaphoreSlim

Task.WhenAll 會讓做好的 task 全部同時執行。 對象件數多的話 HTTP 連線、DB 連線、記憶體用量、對外部服務的負荷會一起增加。

這種時候 決定同時多少件執行 比較穩定。

Parallel.ForEachAsync 意圖相當好讀。

public async Task DownloadAndSaveAsync(IEnumerable<string> urls, CancellationToken cancellationToken)
{
    var options = new ParallelOptions
    {
        MaxDegreeOfParallelism = 8,
        CancellationToken = cancellationToken
    };

    await Parallel.ForEachAsync(
        urls.Select((url, index) => (url, index)),
        options,
        async (item, token) =>
        {
            string html = await _httpClient.GetStringAsync(item.url, token);
            string path = Path.Combine("cache", $"{item.index}.html");
            await File.WriteAllTextAsync(path, html, token);
        });
}

這個模式適合的是,

  • 件數多
  • 各項目處理獨立
  • 但避免全件一齊

這情況。

另一方面,想更自由控制用 SemaphoreSlim 也行。 例如「特定外部 API 同時只 4 件」這類控制。

簡言之,

  • 幾件用 Task.WhenAll
  • 大量件數用 Parallel.ForEachAsyncSemaphoreSlim

這樣思考容易整理。

3.7. 想依序流的話用 Channel<T>

「不用馬上結束但想確實處理」的工作,想從呼叫端分離的情況存在。 郵件傳送、日誌轉送、Webhook 的後處理、檔案轉換等。

這時若用 Task.Run 扔出去,

  • 例外在哪看
  • 終止時等待嗎
  • 件數增加到哪接受

會曖昧。

這類工作 放佇列讓專用 consumer 依序處理 比較易管理。

flowchart LR
    p["producer"] --> w["WriteAsync"]
    w --> q{"佇列有空嗎?"}
    q -- "是" --> c["進 Channel"]
    q -- "否" --> b["等到空"]
    c --> d["consumer ReadAsync"]
    d --> e["依序 await 處理"]

Channel<T> 能相當自然地寫 producer / consumer 形式。

public sealed class BackgroundTaskQueue
{
    private readonly Channel<Func<CancellationToken, ValueTask>> _queue =
        Channel.CreateBounded<Func<CancellationToken, ValueTask>>(
            new BoundedChannelOptions(100)
            {
                FullMode = BoundedChannelFullMode.Wait
            });

    public ValueTask EnqueueAsync(
        Func<CancellationToken, ValueTask> workItem,
        CancellationToken cancellationToken = default)
    {
        ArgumentNullException.ThrowIfNull(workItem);
        return _queue.Writer.WriteAsync(workItem, cancellationToken);
    }

    public ValueTask<Func<CancellationToken, ValueTask>> DequeueAsync(CancellationToken cancellationToken)
        => _queue.Reader.ReadAsync(cancellationToken);
}

本例的 BoundedChannelFullMode.Wait佇列滿就讓寫入端等待 的設定。 這就是 backpressure。

ASP.NET Core 的話把這種佇列與 BackgroundService 組合消費的形式易懂。 比起「真的 fire-and-forget」,這邊易處理例外、停止、並行數、上限。

3.8. 一定間隔執行用 PeriodicTimer

一定間隔的非同步處理,PeriodicTimer 相當好讀。

public async Task RunPeriodicAsync(CancellationToken cancellationToken)
{
    using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10));

    while (await timer.WaitForNextTickAsync(cancellationToken))
    {
        await RefreshCacheAsync(cancellationToken);
    }
}

這寫法的好處,

  • 比 callback 型 Timer 易追流程
  • 能以 await 為基礎寫
  • 停止時能自然使用 CancellationToken

注意點是,PeriodicTimer不對 1 個 timer 同時丟多個 WaitForNextTickAsync 為前提。 另外處理時間比週期長,該延遲需要作為設計處理。 timer 不會擅自並行化追上。

3.9. 逐次到達的資料用 IAsyncEnumerable<T>

比起把全件放 List<T> 再回,想從到達的依序處理 的場面存在。

  • 依序讀分頁 API
  • 一點一點讀檔案行
  • 把串流結果直接流出

這時 IAsyncEnumerable<T>await foreach 自然。

public async Task ProcessUsersAsync(CancellationToken cancellationToken)
{
    await foreach (User user in _userRepository.StreamUsersAsync(cancellationToken))
    {
        await ProcessUserAsync(user, cancellationToken);
    }
}

這形式適合,

  • 不想等全件齊
  • 想一件一件處理
  • 不想全放記憶體

這情況。

回傳值用 Task<List<T>> 還是 IAsyncEnumerable<T> 取決於 結果是全齊再用還是依到達順序使用

3.10. 想非同步釋放就用 await using

釋放時需要 flush 或通訊結束等非同步處理的型,實作 IAsyncDisposable。 這時不用 using 而用 await using

public async Task WriteFileAsync(string path, byte[] data, CancellationToken cancellationToken)
{
    await using var stream = new FileStream(
        path,
        FileMode.Create,
        FileAccess.Write,
        FileShare.None,
        bufferSize: 81920,
        useAsync: true);

    await stream.WriteAsync(data, cancellationToken);
}

重點是,

  • IAsyncDisposable 就用 await using
  • 「開」同步但「關」非同步是常見的

想避免「寫入 async,最後釋放只有同步」的偏移時有效。

3.11. 跨 await 的互斥用 SemaphoreSlim

await 的程式碼,有用 SemaphoreSlim 代替 lock 的場面。

public sealed class CacheRefresher
{
    private readonly SemaphoreSlim _gate = new(1, 1);

    public async Task RefreshAsync(CancellationToken cancellationToken)
    {
        await _gate.WaitAsync(cancellationToken);
        try
        {
            await RefreshCoreAsync(cancellationToken);
        }
        finally
        {
            _gate.Release();
        }
    }

    private static Task RefreshCoreAsync(CancellationToken cancellationToken)
        => Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
}

重要是,

  • WaitAsync 進入
  • Releasefinally 一定呼叫

這 2 點。

「想同時只 1 件進入」「想外部 API 呼叫同時到 3 件」 這類場面,SemaphoreSlim 相當實用。

3.12. UI / 應用程式程式碼 / 函式庫區分 await 的寫法

ConfigureAwait(false) 不是隨時加就好。

先粗略分如下思考容易整理。

flowchart LR
    a["UI / 應用程式程式碼"] --> b["await someAsync()"]
    b --> c["回到原 context 繼續"]

    d["通用函式庫"] --> e["await someAsync().ConfigureAwait(false)"]
    e --> f["不持有回到特定 context 的前提"]
  • UI / 應用程式程式碼
    • 先用普通 await 即可
    • await 後要做 UI 更新或依賴應用程式 context 的處理,不加 ConfigureAwait(false) 較自然
  • ASP.NET Core 的應用程式程式碼
    • 通常 await 就夠
    • 不用把 ConfigureAwait(false) 作為整體作法硬性堅持
  • 通用函式庫程式碼
    • 不依賴 UI 或應用程式模型的話 ConfigureAwait(false) 有力

簡言之,

  • 應用程式端用 plain await
  • 通用函式庫檢討 ConfigureAwait(false)

這樣想實務上相當容易整理。

4. 寫法的基本規則

4.1. 回傳值先用 Task / Task<T>

async 方法的回傳值先以下思考。

回傳值 先的思考方式
Task 無回傳值的 async 方法的基本
Task<T> 回傳值的 async 方法的基本
ValueTask / ValueTask<T> 計測後看到需求再選

ValueTask 看起來方便但 不一定比 Task。 是結構所以有複製成本,用法也有限制。

特別重要是 ValueTask 基本以只 await 1 次為前提。 輕率放本地變數多次等待不合適。

所以日常應用程式程式碼中,先用 Task / Task<T> 就夠。

另外 方法名加 Async 後綴 較易懂。

public Task SaveAsync(CancellationToken cancellationToken)
{
    return Task.CompletedTask;
}

public Task<int> CountAsync(CancellationToken cancellationToken)
{
    return Task.FromResult(_count);
}

如上,沒 await 處理的話,不勉強加 async,回傳 Task.CompletedTaskTask.FromResult 較自然。

4.2. async void 只用事件處理器

async void 除事件處理器外基本避免。

理由單純,

  • 呼叫端不能 await
  • 不能等完成
  • 例外處理變難
  • 難測試

只有事件處理器需要 void,只在那裡用。

private async void SaveButton_Click(object? sender, EventArgs e)
{
    try
    {
        await SaveAsync(_saveCancellation.Token);
        _statusLabel.Text = "已儲存。";
    }
    catch (OperationCanceledException)
    {
        _statusLabel.Text = "已取消。";
    }
    catch (Exception ex)
    {
        MessageBox.Show(this, ex.Message, "儲存錯誤");
    }
}

事件處理器中 裡面抓例外並送回 UI 側 的意識重要。

4.3. 接收 CancellationToken 往下流傳

可取消的操作就接 CancellationToken 直接往下流傳。

public async Task<string> DownloadTextAsync(string url, CancellationToken cancellationToken)
{
    using HttpResponseMessage response = await _httpClient.GetAsync(url, cancellationToken);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync(cancellationToken);
}

常見的是上位接 token 卻沒往下流傳。 這樣「看似能取消卻中途停不下來」的程式碼容易。

另外 逾時 也因「只對等待加上限」還是「想停實處理」意義改變。

  • 只對等待加上限: WaitAsync
  • 想停實處理: CancellationTokenSource.CancelAfter 和 token 的傳播

這差異容易事後變缺陷,最初定好比較穩定。

4.4. 非同步 API 盡量全程非同步串接

async / await 的話,盡量全程非同步串接 較自然。

置換的指標如下。

想寫的寫法 置換目標
Task.Result / Task.Wait() await
Task.WaitAll() await Task.WhenAll(...)
Task.WaitAny() await Task.WhenAny(...)
Thread.Sleep(...) await Task.Delay(...)

特別 UI 或 ASP.NET Core 混同步等待 卡住方式難讀。

現在 C# 也可用 async Task Main(),主控台應用程式勉強同步化的理由也相當減少。

4.5. 用 LINQ 做 task 用 ToArray / ToList 確定

Task.WhenAllTask.WhenAny 與 LINQ 組合, 用 ToArray()ToList() 先確定比較安全。

Task<User>[] tasks = userIds
    .Select(id => _userRepository.GetAsync(id, cancellationToken))
    .ToArray();

User[] users = await Task.WhenAll(tasks);

理由是 LINQ 延遲執行。 「以為全開始了」讀,結果還沒列舉,樸素地危險。

  • 想全件一起等用 ToArray()
  • 想中途刪除・替換用 ToList()

這樣記容易區分。

5. 常見的反面模式

反面模式 什麼痛苦 先的置換
Task.Run(async () => await IoAsync()) 把 I/O 等待白白重丟 await IoAsync()
Task.Result / Wait() 阻塞執行緒,易卡 await
在 async 流程混 Thread.Sleep() 等待中也佔用執行緒 Task.Delay()
在普通方法用 async void 不能等,例外難管理 Task / Task<T>
該用 Task.WhenAll 的場面直列 await 不必要地變慢 先全部啟動再 WhenAll
對大量件數用 WhenAll 一次丟 負荷跳 Parallel.ForEachAsync / SemaphoreSlim
試圖用 lock 跨 await 不符合目的 SemaphoreSlim.WaitAsync
用純 Task.Runfire-and-forget 例外・停止・上限管理曖昧 Channel<T> / BackgroundService
對 UI 程式碼機械地加 ConfigureAwait(false) await 後的 UI 更新易壞 plain await
ValueTask 當標準 複雜度相對獲益不大 先用 Task

此表中特別實務上常見的是以下 3 個。

  1. I/O 卻 Task.Run
  2. 本來獨立卻直列 await
  3. fire-and-forget 沒壽命管理

只改這 3 個程式碼可讀性就相當改善。

6. 審查時的檢核表

審查 async / await 周邊時依序看易懂。

  • 該處理是 I/O-bound 還是 CPU-bound,最初能以言語說明嗎
  • 是否殘留 Task.Result / Task.Wait() / Thread.Sleep()
  • 是否用 Task.Run 包 I/O 等待
  • 獨立的處理是否不必要地直列 await
  • 反過來,大量件數是否無限制 WhenAll
  • CancellationToken 是否正確往下流傳
  • async void 是否只在事件處理器
  • fire-and-forget 的話例外・停止・上限由誰管理是否定好
  • SemaphoreSlim 的話 Release 是否在 finally
  • ValueTask 的話是否有計測上的理由,是否以 1 次 await 為前提
  • ConfigureAwait(false) 的有無是否符合程式碼種類
    • UI / 應用程式程式碼用 plain await
    • 通用函式庫檢討 ConfigureAwait(false)

這檢核表團隊對齊審查觀點也好用。

7. 大致的使用區分

想做的事 先選的
1 件的 HTTP / DB / 檔案 I/O 直接 await async API
不想停 UI 的沉重計算 Task.Run
獨立的少數非同步處理 Task.WhenAll
只要最先的 1 件 Task.WhenAny
大量件數加上限轉 Parallel.ForEachAsync / SemaphoreSlim
有順序的背景處理 Channel<T>
一定間隔轉 PeriodicTimer
處理逐次 stream IAsyncEnumerable<T> / await foreach
跨 await 的互斥 SemaphoreSlim
通用函式庫 檢討 ConfigureAwait(false)
回傳值型迷惘 先用 Task / Task<T>

8. 總結

async / await 的最佳實踐比起記多個細節技巧, 按處理種類選擇型 這整理實務上有效。

先看的順序大致如下。

  1. 區分 I/O 等待還是 CPU 計算
  2. I/O 就直接 await async API
  3. CPU 計算決定在哪執行
  4. 多處理選 WhenAll / WhenAny / 並行數限制
  5. 脫離請求壽命不要純 fire-and-forget,做成佇列
  6. 統一回傳值、取消、例外、互斥、context 的處理

async / await 寫法本身簡潔。 但粗略使用方針會難看見。

反過來,

  • I/O 當 I/O 處理
  • CPU 當 CPU 處理
  • 背景處理當背景處理管理壽命

分開這 3 個就會相當易讀。

9. 參考資料

共用相同標籤的最新文章。能以相近的主題延伸理解。

與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。

本文連結到以下服務頁面,歡迎從最接近的入口查看。

作者檔案

本文作者的個人檔案頁面。

Go Komura

小村軟體有限公司 代表

以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。

回到部落格一覽