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

· 更新日期: · · C#, async/await, .NET, 設計

更新紀錄(僅初版,2026年03月09日 發布)
初次發布
引用本文(DOI: 10.5281/zenodo.21616243)

本文保存於 Zenodo。以下同時提供一律指向最新版本的 DOI,以及固定於您正在閱讀版本的 DOI。

小村 豪(2026)。〈C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表〉。小村軟體有限公司。https://doi.org/10.5281/zenodo.21616243 https://staging.comcomponent.com/zh-TW/blog/2026/03/09/001-csharp-async-await-best-practices/

DOI(最新版本)
10.5281/zenodo.21616243
DOI(此版本)
10.5281/zenodo.21616244

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
UI 事件 / 桌面ASP.NET Core 請求worker / 背景等全部完成用最先結束的件數多依序流一定間隔逐次 stream想做的處理等外部 I/O?直接 await async APICPU 計算重?在哪執行?考慮 Task.Run不要用 Task.Run 包必要時移到別 worker 或佇列當場執行或明確並行度處理多件工作?Task.WhenAllTask.WhenAnyParallel.ForEachAsync或 SemaphoreSlimChannel&lt;T&gt;PeriodicTimerIAsyncEnumerable&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 會開始。

Task 3Task 2Task 1呼叫端Task 3Task 2Task 1呼叫端開始開始開始await Task.WhenAll(...)完成完成完成

這個模式適合的是,

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

這情況。

件數多的話如後面的 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 依序處理 比較易管理。

producerWriteAsync佇列有空嗎?進 Channel等到空consumer ReadAsync依序 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) 不是隨時加就好。

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

UI / 應用程式程式碼await someAsync()回到原 context 繼續通用函式庫await someAsync().ConfigureAwait(false)不持有回到特定 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. 參考資料

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

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

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

常見問題

整理諮詢這個主題時常見的問題。

什麼時候該用 Task.Run?
Task.Run 有效的是想把 CPU 計算從當前執行緒移出時,例如在 UI 事件處理器中執行沉重計算會讓畫面停住,這時用 Task.Run 很自然。反過來,I/O 等待不需要 Task.Run,直接 await async API 即可;用 Task.Run 包已經 async 的 I/O 只是把等待白白丟到別的執行緒。ASP.NET Core 的請求處理本來就在 ThreadPool 運作,夾一層 Task.Run 後立即 await 只會增加多餘的排程。
ConfigureAwait(false) 該加在哪裡?
不依賴 UI 或應用程式特有 context 的通用函式庫程式碼,加 ConfigureAwait(false) 很有力。UI 或應用程式端程式碼先用一般的 await 即可,await 後要更新 UI 的地方更不該加,機械式地到處加反而容易弄壞 UI 更新。ASP.NET Core 的應用程式程式碼通常 await 就夠,不需要把 ConfigureAwait(false) 當成整體規範硬性堅持。
fire-and-forget 的背景處理該怎麼寫?
用純 Task.Run 扔出去,例外在哪看、終止時等不等、件數增加到哪接受等問題都會變得曖昧。真要與呼叫端分離壽命,放到 Channel<T> 或 HostedService 等受管理的位置比較穩定。例如用有上限的 bounded Channel 搭配 BackgroundService 消費,佇列滿時讓寫入端等待(backpressure),例外、停止、並行數、上限都比較好管理。
多個獨立的非同步處理該怎麼等?
互不依賴的少數處理,先全部啟動再用 Task.WhenAll 一起等,比一件一件直列 await 自然。要注意 LINQ 是延遲執行,用 Select 建 task 後要以 ToArray() 或 ToList() 確定,所有 task 才會真正開始。件數多時不要用 WhenAll 全部同時丟,改用 Parallel.ForEachAsync 或 SemaphoreSlim 決定並行數上限,避免連線數、記憶體與外部服務負荷一起暴增。

作者檔案

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

Go Komura

小村軟體有限公司 代表

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

回到部落格一覽