以一頁整理 WPF / WinForms 的 async/await 和 UI 執行緒 - await 後的回歸處、Dispatcher、ConfigureAwait、.Result / .Wait() 的卡點
· 小村 豪 · C#, async/await, .NET, WPF, WinForms, UI, 執行緒
在 WPF / WinForms 使用 async / await 時最容易迷惘的是 await 後要回到哪個執行緒,以及 什麼時候可以碰 UI。
特別是 Dispatcher、BeginInvoke、ConfigureAwait(false)、.Result / .Wait() 混在一起時,畫面凍結或跨執行緒例外的原因會變得難以看見。
本文限定於整理 WPF / WinForms 的 UI 執行緒與 async / await 的關係。
async / await 的整體判斷軸與 C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表 連結。
實務上真正有血腥味的大致是這一帶。
await後不知道接續在哪裡運作- 不知道夾了
Task.Run之後可不可以碰 UI ConfigureAwait(false)該加在哪裡迷惘- 因
.Result/.Wait()/.GetAwaiter().GetResult()導致畫面凍結 - WPF 的
Dispatcher和 WinForms 的Invoke/BeginInvoke/InvokeAsync在腦中混在一起
WPF / WinForms 兩者都是 UI 執行緒中心的模型。
所以整理 async / await 時最有效的不是「非同步是什麼」這類哲學的話題,而是明確 對 UI 執行緒與訊息迴圈在做什麼。
本文以 .NET 6 以後的 WPF / WinForms 應用程式 為前提,
以實務上好用的順序整理 await 後的回歸處、Dispatcher、ConfigureAwait(false)、.Result / .Wait() 的卡住原因。
此外,WinForms 的 Control.InvokeAsync 是 .NET 9 以後。
之前的 WinForms 基本使用 BeginInvoke / Invoke。
1. 先講結論(一句話)
- WPF / WinForms 的 UI 事件處理器 中 plain
await的情況,await後的接續可以當作基本上回到 UI 執行緒來思考 Task.Run是 把 CPU 計算從 UI 執行緒移出的工具,不是包裹 I/O 等待的工具- 在 UI 處理器中
await Task.Run(...),只要那個await是 plainawait,接續通常回到 UI 執行緒 ConfigureAwait(false)是「不強制回到該await所捕捉的 UI 上下文」的意思。加了之後的接續中直接碰 UI 很危險.Result/.Wait()/.GetAwaiter().GetResult()會阻塞 UI 執行緒。當await的接續需要回到 UI 時相當普遍地會卡住- WPF 中要明確回到 UI 的話用
Dispatcher.InvokeAsync - WinForms 中要明確回到 UI 的話,舊式是
BeginInvoke,.NET 9 以後的InvokeAsync與 async 流程相性好 - 先的方針是 UI 最外側用 plain
await、通用函式庫考慮ConfigureAwait(false)、回到 UI 只在必要的地方明確寫
簡言之,WPF / WinForms 中
- 現在在哪個執行緒運作
await的接續回到哪裡- 回到 UI 的責任由哪裡承擔
看這 3 個就會相當容易整理。
2. 先用一頁整理
2.1. 整體圖
先用這個圖大致掌握整體圖比較快。
flowchart LR
A["UI 事件處理器<br/>(WPF / WinForms)"] --> B["plain await<br/>I/O API"]
B --> C["捕捉 UI SynchronizationContext"]
C --> D["await 後在 UI 執行緒恢復"]
D --> E["可以直接寫 UI 更新"]
A --> F["await Task.Run(...)<br/>沉重 CPU 處理"]
F --> G["計算本體在 ThreadPool"]
G --> H["await 後在 UI 執行緒恢復"]
H --> E
A --> I["await SomeAsync().ConfigureAwait(false)"]
I --> J["不強制回到 UI"]
J --> K["接續是任意執行緒"]
K --> L["直接 UI 更新危險<br/>需要 Dispatcher / Invoke"]
A --> M["SomeAsync().Result / Wait()<br/>GetAwaiter().GetResult()"]
M --> N["阻塞 UI 執行緒"]
N --> O["接續無法回到 UI"]
O --> P["掛起 / 死結 / 至少凍結"]
在實務上看,大致是以下 4 種模式。
- 在 UI 事件處理器中 plain
await - 在 UI 事件處理器中用
Task.Run逃離 CPU - 用
ConfigureAwait(false)拿掉回歸處 - 用
.Result/.Wait()阻塞 UI 執行緒
2.2. 先的判斷表
| 狀況 | 等待中哪裡在運作 | await 後的接續 |
是否可以直接碰 UI | 先的選擇 |
|---|---|---|---|---|
在 UI 處理器 await SomeIoAsync() |
I/O 的完成等待。UI 執行緒本身可以回訊息迴圈 | 基本上是 UI 執行緒 | 可以 | plain await |
在 UI 處理器 await Task.Run(...) |
沉重 CPU 是 ThreadPool | 基本上是 UI 執行緒 | 可以 | 只用 Task.Run 包 CPU |
在 UI 處理器 await x.ConfigureAwait(false) |
不把回歸處固定在 UI | 任意執行緒 | 不可 | UI 程式碼中基本避免 |
在 UI 執行緒 x.Result / x.Wait() |
UI 執行緒被等待堵住 | 接續本身就難以轉動 | 不可 | 不使用 |
想在背景執行緒或 ConfigureAwait(false) 後更新 UI |
在與 UI 不同的執行緒運作 | 原樣不是 UI | 不可 | Dispatcher.InvokeAsync / BeginInvoke / InvokeAsync |
這張表重要的是 plain await 在 UI 程式碼中反而是盟友。
敵人不是 await 本身,而是 同步地阻塞 UI 執行緒。
3. 本文使用的用語
3.1. UI 執行緒與訊息迴圈
WPF / WinForms 的 UI 基本上是 有 1 條 UI 執行緒,由那裡處理輸入・繪圖・事件處理 的形式。
這個 UI 執行緒大致有以下角色。
- 處理按鈕按下、鍵盤輸入、重繪等訊息
- 成為能安全碰控制項或 UI 物件的唯一執行緒
- 那裡塞入太多處理,畫面更新或輸入回應就停止
這裡的核心是 UI 執行緒的工作是「快速轉動」。 長時間阻塞這裡,滑鼠、鍵盤、重繪都會卡住,從使用者看起來就是「凍結了」。
這個印象用下面的圖保留會相當容易整理。
flowchart LR
A["使用者輸入 / 重繪要求"] --> B["UI 執行緒的訊息迴圈"]
B --> C["事件處理器執行"]
C --> D["畫面更新"]
D --> B
C --> E["長的同步處理"]
E --> F["訊息迴圈不轉動"]
F --> G["看起來畫面凍結"]
3.2. SynchronizationContext / Dispatcher / Invoke
這裡常出現的用語以實務角度粗略分開如下。
| 用語 | 這裡的意思 |
|---|---|
| UI 執行緒 | 建立 UI 物件的執行緒。基本只有這裡能安全碰 UI |
| 訊息迴圈 | UI 執行緒依序處理訊息的機制 |
SynchronizationContext |
為了「把處理送回該執行位置」的抽象化 |
Dispatcher |
WPF 的 UI 執行緒用佇列 |
Invoke / BeginInvoke / InvokeAsync |
往 UI 執行緒丟處理的 API |
細節來說,await 決定接續處時優先使用 目前的 SynchronizationContext,沒有的話也看 非預設的 TaskScheduler。
但在 WPF / WinForms 的實務中,先認為 UI 的 SynchronizationContext 有效 就夠。
各框架的對應大致如下看比較好懂。
| 框架 | UI 側的上下文 | 明確回到 UI 的代表 API |
|---|---|---|
| WPF | DispatcherSynchronizationContext |
Dispatcher.InvokeAsync / Dispatcher.BeginInvoke / Dispatcher.Invoke |
| WinForms | WindowsFormsSynchronizationContext |
Control.BeginInvoke / Control.Invoke / .NET 9+ Control.InvokeAsync |
WPF 以 Dispatcher 為中心。
WinForms 以控制項的控制碼和訊息迴圈為中心,BeginInvoke / Invoke 會浮到表面。
在實務上,抽象化和實體的關係記住這個程度就不容易混。
flowchart TD
A["目前的程式碼"] --> B["SynchronizationContext"]
B --> C["WPF: DispatcherSynchronizationContext"]
B --> D["WinForms: WindowsFormsSynchronizationContext"]
C --> E["Dispatcher.InvokeAsync / BeginInvoke / Invoke"]
D --> F["Control.BeginInvoke / Invoke / InvokeAsync(.NET 9+)"]
4. 典型模式
4.1. 在 UI 事件處理器中 plain await
最自然的形式。
private async void LoadButton_Click(object sender, RoutedEventArgs e)
{
LoadButton.IsEnabled = false;
StatusText.Text = "讀取中...";
try
{
string text = await File.ReadAllTextAsync(FilePathTextBox.Text);
PreviewTextBox.Text = text;
StatusText.Text = "完成";
}
catch (Exception ex)
{
StatusText.Text = ex.Message;
}
finally
{
LoadButton.IsEnabled = true;
}
}
這段程式碼中,LoadButton_Click 在 UI 執行緒開始。
然後 await File.ReadAllTextAsync(...) 是 plain await,通常會捕捉當時的 UI 上下文。
所以,
- 檔案 I/O 等待中不佔用 UI 執行緒
- 讀取完成後的接續基本上回到 UI 執行緒
PreviewTextBox.Text = text;可以直接寫
是這樣的形式。
這裡多餘的 Dispatcher 不需要。
在 UI 處理器中只做 plain await 的話,通常就能直接碰 UI。
WinForms 的看法也相同。
只要在 Click 處理器中做 plain await,接續基本上回到 UI 側。
畫成圖是這樣的流程。
sequenceDiagram
participant UI as UI 執行緒
participant IO as 非同步 I/O
participant Ctx as UI SynchronizationContext
UI->>UI: Click 處理器開始
UI->>IO: await ReadAllTextAsync
UI-->>Ctx: 預約接續回到 UI
Note over UI: 等待中回到訊息迴圈
IO-->>Ctx: I/O 完成
Ctx-->>UI: 在 UI 執行緒恢復接續
UI->>UI: 更新 TextBox / Label
4.2. 只用 Task.Run 包沉重 CPU 計算
Task.Run 有效的是 想把沉重 CPU 計算從 UI 執行緒移出時。
private async void HashButton_Click(object sender, RoutedEventArgs e)
{
HashButton.IsEnabled = false;
ResultText.Text = "計算中...";
try
{
byte[] data = await File.ReadAllBytesAsync(InputPathTextBox.Text);
string hash = await Task.Run(() =>
{
using SHA256 sha256 = SHA256.Create();
byte[] digest = sha256.ComputeHash(data);
return Convert.ToHexString(digest);
});
ResultText.Text = hash;
}
catch (Exception ex)
{
ResultText.Text = ex.Message;
}
finally
{
HashButton.IsEnabled = true;
}
}
這段程式碼中發生的事大致如下。
- 事件處理器在 UI 執行緒開始
File.ReadAllBytesAsync的 I/O 等待以非同步流動- 只把沉重的雜湊計算用
Task.Run送到 ThreadPool await Task.Run(...)的接續是 plainawait所以回到 UI 執行緒ResultText.Text = hash;可以直接寫
也就是 只有 Task.Run 的裡面是別的執行緒。
並不是 await 後永久地去「已經不是 UI 的地方」。
這裡用一頁看不容易誤解。
sequenceDiagram
participant UI as UI 執行緒
participant IO as 非同步 I/O
participant Pool as ThreadPool
UI->>IO: await ReadAllBytesAsync
IO-->>UI: 因為是 plain await 所以在 UI 恢復
UI->>Pool: 用 Task.Run 丟沉重的 CPU 處理
Pool-->>UI: 返回計算結果
Note over UI: await Task.Run(...) 的接續在 UI 恢復
UI->>UI: 結果反映到畫面
這裡的注意有 2 個。
- 不要用
Task.Run包 I/O 等待 - 把
Task.Run當作不是「非同步化」而是「建立 CPU 的逃避處」
Task.Run(async () => await File.ReadAllTextAsync(...)) 這種寫法只是把 I/O 等待白白地再丟回 ThreadPool,沒什麼好處。
4.3. ConfigureAwait(false) 不是「不回去的保證」而是「不強制回去」
這裡最容易被誤解。
首先,ConfigureAwait(false) 適合的是 不依賴 UI 或特定應用程式模型的通用函式庫程式碼。
public sealed class DocumentRepository
{
public async Task<string> LoadNormalizedTextAsync(string path, CancellationToken cancellationToken)
{
string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
return text.Replace("\r\n", "\n", StringComparison.Ordinal);
}
}
這個方法不碰 UI。
在 WPF、WinForms、ASP.NET Core、worker 都能用的形式。
這種程式碼中 ConfigureAwait(false) 相當自然。
然後 UI 側的呼叫用 plain await 就好。
private readonly DocumentRepository _repository = new();
private async void OpenButton_Click(object sender, RoutedEventArgs e)
{
OpenButton.IsEnabled = false;
StatusText.Text = "讀取中...";
try
{
string text = await _repository.LoadNormalizedTextAsync(
PathTextBox.Text,
CancellationToken.None);
PreviewTextBox.Text = text;
StatusText.Text = "完成";
}
catch (Exception ex)
{
StatusText.Text = ex.Message;
}
finally
{
OpenButton.IsEnabled = true;
}
}
這裡重要的是 函式庫內的 ConfigureAwait(false) 不會把呼叫端的 await 也強制變 false。
也就是,
- 函式庫內部不回 UI
- UI 處理器 plain
await它,呼叫端的接續回到 UI
這樣的分離可行。
相反地,在 UI 處理器本身這樣寫很危險。
private async void OpenButton_Click(object sender, RoutedEventArgs e)
{
string text = await _repository.LoadNormalizedTextAsync(
PathTextBox.Text,
CancellationToken.None).ConfigureAwait(false);
PreviewTextBox.Text = text;
}
這個情況,OpenButton_Click 的那個 await 的接續不強制回 UI。
所以 PreviewTextBox.Text = text; 可能變成 跨執行緒存取。
另一點樸素重要的。
ConfigureAwait(false) 不是「一定移到 ThreadPool」。
那個 await 不等待就立即完成時,接續可能就在當前執行緒流動。
所以 ConfigureAwait(false) 不是
- 「一定去別的執行緒」
- 「從這裡以後一直不是 UI」
的意思。
意思上始終是
- 不強制該
await的接續回到原本的 UI 上下文
這樣理解相當不容易出事。
整理成圖是這樣看。
flowchart LR
A["UI 處理器 await"] --> B{"加 ConfigureAwait(false)?"}
B -- 否 --> C["接續基本上是 UI 執行緒"]
C --> D["直接 UI 更新容易"]
B -- 是 --> E["接續不固定在 UI"]
E --> F["可能在任意執行緒恢復"]
F --> G["UI 更新需要 Dispatcher / Invoke"]
4.4. .Result / .Wait() / .GetAwaiter().GetResult() 卡住的原因
這是最常見的事故。
private void LoadButton_Click(object sender, RoutedEventArgs e)
{
string text = LoadTextAsync().Result;
PreviewTextBox.Text = text;
}
private async Task<string> LoadTextAsync()
{
string text = await File.ReadAllTextAsync(FilePathTextBox.Text);
return text.ToUpperInvariant();
}
乍看只是同步取結果。 但在 UI 執行緒相當危險。
流程畫成圖是這樣。
sequenceDiagram
participant UI as UI 執行緒
participant IO as 非同步 I/O
participant Ctx as UI SynchronizationContext
UI->>UI: LoadButton_Click 開始
UI->>IO: 呼叫 LoadTextAsync()
IO-->>UI: 回傳未完成的 Task
UI->>UI: .Result 等待並阻塞
IO-->>Ctx: I/O 完成,想把接續送回 UI
Ctx-->>UI: 想執行接續
Note over UI: 但 UI 被 .Result 堵住
Note over UI, Ctx: 接續無法轉動所以無法完成
用話描述發生的事,如下。
- UI 執行緒呼叫
LoadTextAsync() LoadTextAsync()內的await捕捉 UI 上下文- UI 執行緒在
.Result等待 - I/O 結束
LoadTextAsync()的接續想回 UI 執行緒- 但 UI 執行緒被
.Result堵住 - 接續無法跑所以
LoadTextAsync()無法完成 .Result不會結束
也就是 UI 說「等你結束」,非同步側說「能回 UI 就能結束」,互相等待。 相當討厭的感覺。
這裡常見的誤解是認為用 GetAwaiter().GetResult() 比較安全。
但是 阻塞 UI 執行緒 的本質相同。差異主要是例外被包裝的方式。
所以 UI 中把這 3 個當作同樣氣味來處理比較安全。
.Result.Wait().GetAwaiter().GetResult()
另外,對 WPF 的 Dispatcher.InvokeAsync(...) 回傳的 Task 做 Task.Wait() 也危險。
WPF 的文件也寫著對 DispatcherOperation 回傳的 Task 做 Task.Wait 會死結。
簡言之,在 UI 的脈絡中「以同步等待丟出去的」方向本身 相當容易卡。
「一定會死結嗎」的話,不一定。 剛好接續不回 UI 的程式碼可能不死結只是讓 UI 凍結。 但那也足夠痛苦,所以 UI 中基本上不做比較好。
5. 何時使用 Dispatcher / Invoke
整理下來,plain await 的 UI 處理器 平常不需要明確的 Dispatcher / Invoke。
需要的是例如以下情況。
- 想在
ConfigureAwait(false)的接續碰 UI - 在
Task.Run中或外側也做成不回 UI 的構成 - socket 接收、計時器、事件回呼等從一開始就不是 UI 執行緒的地方接收通知
- 把 UI 和非 UI 有意分離的層中,只想明確寫最後的 UI 更新
WPF 的代表是 Dispatcher.InvokeAsync。
private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
await Dispatcher.InvokeAsync(() =>
{
PreviewTextBox.Text = text;
StatusText.Text = "完成";
});
}
WinForms 的話 .NET 9 以後 InvokeAsync 相性相當好。
private async Task RefreshPreviewAsync(string path, CancellationToken cancellationToken)
{
string text = await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false);
await previewTextBox.InvokeAsync(() =>
{
previewTextBox.Text = text;
statusLabel.Text = "完成";
});
}
WinForms 的舊模式使用 BeginInvoke。
Invoke 是同步傳送會讓呼叫端等待。BeginInvoke 投遞後立刻回傳。
async 流程中基本上不阻塞的一側比較合適。
粗略說用以下辨識法就夠。
| 想做的事 | WPF | WinForms |
|---|---|---|
| 同步放入 UI | Dispatcher.Invoke |
Control.Invoke |
| 非同步丟給 UI | Dispatcher.InvokeAsync / Dispatcher.BeginInvoke |
Control.BeginInvoke / .NET 9+ Control.InvokeAsync |
| 想自然配合 async / await | Dispatcher.InvokeAsync |
.NET 9+ Control.InvokeAsync,之前是 BeginInvoke |
實務上的感覺是,
- UI 處理器只做 plain
await的話不需要 - 從 UI 以外的地方想碰 UI 時使用
- async 流程中不要過度增加同步
Invoke
這樣可以相當減少事故。
迷惘時以下判斷圖就夠。
flowchart TD
A["寫這個接續的地方是 UI 執行緒?"] --> B{"是?"}
B -- 是 --> C["可以 plain await 直接 UI 更新"]
B -- 否 --> D{"想碰 UI?"}
D -- 否 --> E["直接繼續處理"]
D -- 是 --> F["WPF: Dispatcher.InvokeAsync"]
D -- 是 --> G["WinForms: BeginInvoke / InvokeAsync"]
6. 常見的反面模式
| 反面模式 | 什麼痛苦 | 先的替換 |
|---|---|---|
UI 處理器用 LoadAsync().Result |
阻塞 UI 執行緒。容易死結 | await LoadAsync() |
UI 處理器用 LoadAsync().Wait() |
同上。訊息迴圈停止 | await LoadAsync() |
UI 處理器用 LoadAsync().GetAwaiter().GetResult() |
例外的呈現方式不同但阻塞相同 | await LoadAsync() |
UI 程式碼機械地加 ConfigureAwait(false) |
await 後的 UI 更新容易壞 |
UI 最外側用 plain await |
Task.Run(async () => await IoAsync()) |
白白地再丟 I/O | await IoAsync() |
函式庫程式碼直接握 Dispatcher 或 Control |
UI 依賴深。難以再利用 | 函式庫只回傳資料,UI 側 marshal |
在 async 流程過度使用 Dispatcher.Invoke / Control.Invoke |
容易形成阻塞的環 | 檢討 Dispatcher.InvokeAsync / BeginInvoke / InvokeAsync |
| 建構子或屬性 getter 把 async 同步化 | 成為啟動時掛起的溫床 | 逃到 Loaded / Shown / InitializeAsync |
其中特別容易遇到的是以下 3 個。
- UI 執行緒用
.Result/.Wait() - 對 UI 程式碼機械地加
ConfigureAwait(false) - 函式庫和 UI 的職責混在一起,
Dispatcher侵入深處
光避免這 3 個就會相當平靜。
7. 審查時的檢核表
審查 WPF / WinForms 的 async / await 時,依序看以下比較好懂。
- UI 事件處理器或 UI 初始化路徑是否殘留
.Result/.Wait()/.GetAwaiter().GetResult() Task.Run是否只用在 CPU 計算。是否包了 I/OConfigureAwait(false)是否機械地進入 UI 程式碼- 相反地,通用函式庫是否拖著對 UI 上下文的依賴
await後直接碰 UI 的地方,那裡真的能說是 UI 上下文上嗎- 需要明確回到 UI 的地方是否使用
Dispatcher.InvokeAsync/BeginInvoke/InvokeAsync Dispatcher.Invoke/Control.Invoke這類同步 marshal 是否不必要地增加- 是否從建構子、同步屬性、同步事件強行同步化 async
- 函式庫層是否直接參照
Window/Control/Dispatcher
這個檢核表在團隊對齊「哪裡是 UI 的職責」時也好用。
8. 大致的使用區分
| 想做的事 | 先選的 |
|---|---|
| UI 處理器等待 HTTP / DB / 檔案 I/O | plain await |
| 不想停 UI 的沉重 CPU 計算 | await Task.Run |
ConfigureAwait(false) 後或從背景執行緒更新 UI |
WPF: Dispatcher.InvokeAsync / WinForms: BeginInvoke 或 .NET 9+ InvokeAsync |
| 寫通用函式庫 | 檢討 ConfigureAwait(false) |
| 想在 UI 同步化 async | 基本不做。把呼叫端整體延伸為 async |
| 想做啟動時初始化 | Loaded / Shown / 明確的 InitializeAsync |
想在 await 後直接碰 UI |
UI 最外側保持 plain await |
9. 總結
WPF / WinForms 的 async / await 真正重要的不是
「非同步很難」這樣的氛圍,而是
- 現在從哪裡開始
await的接續回到哪裡- 回到 UI 的責任誰承擔
分開思考。
先的規則以下就相當能戰。
- UI 最外側用 plain
await - 只用
Task.Run包沉重 CPU - 通用函式庫檢討
ConfigureAwait(false) - 需要回到 UI 時才用
Dispatcher/BeginInvoke/InvokeAsync - UI 執行緒不使用
.Result/.Wait()/.GetAwaiter().GetResult()
async / await 本身不是那麼難應付的機制。
但是 不以 UI 執行緒為中心來看就使用的話,會突然變成泥沼。
反過來說,
- 分開 UI 的外側和內側
- 意識回歸處
- 不帶入阻塞
只守這 3 個,WPF / WinForms 的非同步程式碼就會相當安靜。 畫面凍結的程式碼通常不是「非同步不好」,而是 對 UI 執行緒借錢的方式粗糙 而已。
10. 參考資料
- 相關文章:C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表
- Threading Model - WPF
- DispatcherSynchronizationContext Class
- How to handle cross-thread operations with controls - Windows Forms
- WindowsFormsSynchronizationContext Class
- Events Overview - Windows Forms
- TaskScheduler.FromCurrentSynchronizationContext Method
- ConfigureAwait FAQ
- How Async/Await Really Works in C#
- Await, and UI, and deadlocks! Oh my!
- Threading model for WebView2 apps
相關文章
共用相同標籤的最新文章。能以相近的主題延伸理解。
把 Generic Host / BackgroundService 帶進桌面應用程式的理由 - 啟動・壽命・graceful shutdown 的整理會輕鬆很多
整理把 .NET Generic Host 與 BackgroundService 帶進 WPF / WinForms 桌面應用程式的理由,把啟動、lifetime、graceful shutdown 集中於入口管理。透過 StartAsync / ExecuteAsync...
Windows Forms、WPF、WinUI 該選哪個 - 新規開發、既有資產、發佈、UI 表現的判斷表
從既有資產的規模、畫面是表單中心還是表現力中心、現代 Windows UI 是否為產品要件、發佈與運營怎麼跑這四個觀點,整理 WinForms、WPF、WinUI 該選哪個的判斷表,並提醒只想用 Windows App SDK 不必全面遷移到 WinUI。
PeriodicTimer / System.Threading.Timer / DispatcherTimer 的區分使用 - 先整理 .NET 的定期執行
整理 .NET 中 PeriodicTimer、System.Threading.Timer、DispatcherTimer 的差異與使用場景,從執行緒、async 流程、callback 重疊三個角度切入,協助你在 worker、ThreadPool 背景處理及 WPF ...
C# async/await 的最佳實踐 - Task.Run 與 ConfigureAwait 的判斷表
本文以 .NET 6 之後的一般 C# 開發為前提,將 async/await 周邊的判斷整理成可立即查的對照表。先依 I/O 等待與 CPU 計算分流,再決定 Task.Run、Task.WhenAll、Parallel.ForEachAsync、Channel、Peri...
Windows 應用安全處理子行程的 checklist - Job Object、結束傳播、標準輸入輸出、watchdog 的最佳實務
在 Windows 應用上安全處理子行程,關鍵不在挑啟動 API,而是設計行程樹的擁有者與結束流程。本文整理 Job Object 的 KILL_ON_JOB_CLOSE、GUI 與 console 的 graceful shutdown、stdio 平行抽乾與 EOF、w...
相關主題
與本文相近的主題頁面。以本文為起點,可進一步連到相關服務與其他文章。
Windows 技術主題
彙整 KomuraSoft LLC 關於 Windows 開發、故障調查與既有資產活用文章的主題中心。
UI 執行緒 & 計時器
整理 WPF / WinForms UI 執行緒、非同步流程、Dispatcher 使用、計時器判斷的主題頁面。
作者檔案
本文作者的個人檔案頁面。
Go Komura
小村軟體有限公司 代表
以 Windows 軟體開發、技術諮詢與故障調查為中心,在難以重現的故障調查與既有資產仍在運作的專案上具有優勢。