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」。
先看,
- 該處理在等什麼
- 誰擁有該處理的壽命
- 在哪控制同時執行數
這 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<T>"]
q4 -- "一定間隔" --> p9["PeriodicTimer"]
q4 -- "逐次 stream" --> p10["IAsyncEnumerable<T>"]
以下依序看各模式。
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 的請求處理: 基本避免立即
awaitTask.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.ForEachAsync或SemaphoreSlim
這樣思考容易整理。
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進入 Release在finally一定呼叫
這 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)有力
- 不依賴 UI 或應用程式模型的話
簡言之,
- 應用程式端用 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.CompletedTask 或 Task.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.WhenAll 或 Task.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.Run 做 fire-and-forget |
例外・停止・上限管理曖昧 | Channel<T> / BackgroundService |
對 UI 程式碼機械地加 ConfigureAwait(false) |
await 後的 UI 更新易壞 | plain await |
把 ValueTask 當標準 |
複雜度相對獲益不大 | 先用 Task |
此表中特別實務上常見的是以下 3 個。
- I/O 卻
Task.Run - 本來獨立卻直列 await
- 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)
- UI / 應用程式程式碼用 plain
這檢核表團隊對齊審查觀點也好用。
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 的最佳實踐比起記多個細節技巧,
按處理種類選擇型 這整理實務上有效。
先看的順序大致如下。
- 區分 I/O 等待還是 CPU 計算
- I/O 就直接
awaitasync API - CPU 計算決定在哪執行
- 多處理選
WhenAll/WhenAny/ 並行數限制 - 脫離請求壽命不要純 fire-and-forget,做成佇列
- 統一回傳值、取消、例外、互斥、context 的處理
async / await 寫法本身簡潔。
但粗略使用方針會難看見。
反過來,
- I/O 當 I/O 處理
- CPU 當 CPU 處理
- 背景處理當背景處理管理壽命
分開這 3 個就會相當易讀。
9. 參考資料
- Asynchronous programming scenarios - C#
- Asynchronous programming with async and await
- Task-based Asynchronous Pattern (TAP) in .NET
- ConfigureAwait FAQ
- Parallel.ForEachAsync Method
- Task.WaitAsync Method
- System.Threading.Channels library
- Create a Queue Service
- Background tasks with hosted services in ASP.NET Core
- Generate and consume async streams
- Implement a DisposeAsync method
- ValueTask Struct
- CA2012: Use ValueTasks correctly
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
.NET 的 Generic Host 是什麼 - 先整理 DI、設定、日誌、BackgroundService
本文整理 .NET Generic Host 的真面目,並把 DI、設定、日誌、IHostedService 與 BackgroundService 的關係,連同 Host.CreateApplicationBuilder 與 WebApplicationBuilder 的...
.NET 的 Native AOT 是什麼 - 先釐清與 JIT、ReadyToRun、trimming 的差異
把 .NET 的 Native AOT 與 JIT、ReadyToRun、self-contained、single-file、trimming、source generator 放在一起釐清,並從啟動、發布、相依性的角度整理它合適與不合適的情境,幫助讀者判斷該不該採用。
PeriodicTimer / System.Threading.Timer / DispatcherTimer 的區分使用 - 先整理 .NET 的定期執行
整理 .NET 中 PeriodicTimer、System.Threading.Timer、DispatcherTimer 的差異與使用場景,從執行緒、async 流程、callback 重疊三個角度切入,協助你在 worker、ThreadPool 背景處理及 WPF ...
把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多
整理把 .NET Generic Host 與 BackgroundService 帶進 WPF / WinForms 桌面應用程式的理由,把啟動、lifetime、graceful shutdown 集中於入口管理。透過 StartAsync / ExecuteAsync...
以一頁整理 WPF / WinForms 的 async/await 和 UI 執行緒 - await 後的回歸處、Dispatcher、ConfigureAwait、.Result / .Wait() 的卡點
本文以一頁的篇幅整理 WPF 與 WinForms 中 async/await 與 UI 執行緒的關係。從 await 後接續的回歸處、Dispatcher 與 Invoke 的選擇、ConfigureAwait(false) 真正的意義,到 .Result 與 .Wait...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
與本主題相關的服務
本文連結到以下服務頁面,歡迎從最接近的入口查看。
Windows 應用程式開發
支援包含常駐處理、設備連動、運作日誌與可維護結構的 Windows 桌面應用程式。
技術諮詢 & 設計審查
協助釐清設計方向、架構邊界、生命週期責任,以及既有 Windows 資產的處理方式。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。