Asynchronous Programming - 第七章:非同步之林:執行緒的四季輪迴
在這片名為非同步之林的廣闊森林裡,藏著一家智慧餐廳。這間餐廳一天到晚接單不斷,每張訂單都是一個任務,廚房裡的人力有限,卻得讓所有客人都吃得快又好、不讓誰在門口久等,還得同時兼顧廚房資源的高效運用。
這家餐廳裡,有忙碌的櫃台接待員(Main Thread),有後廚大廚團隊(Worker Threads),有專門通知外送到達的取餐小組(I/O Completion Threads),偶爾,老闆還會臨時請來私廚(Custom Thread),用一場又一場執行緒的調度,撐起這座非同步之林裡飲食服務。
🎵 Main Thread = 櫃台接待員
負責第一時間接待客人、記錄每張訂單、回答客人問題。櫃台小姐(Main Thread)不能被卡住,一卡住,後面來的客人全部排隊大塞車!所以櫃台只是登記完,馬上就把訂單丟給後廚(ThreadPool)去做。
✅ 在 ASP.NET 裡,Main Thread 就像每次收到使用者的 HTTP 請求的 Request Thread;桌面程式裡就是 UI 執行緒,負責畫面互動。
🎵 Worker Threads = 後廚大廚團隊
這是餐廳的主要戰力,一群大廚(Worker Threads)在後台幫你煮菜(跑 CPU Bound 工作)。廚師是可彈性調度的,客人多時,廚房會動態叫更多人加班(ThreadPool 的 Hill-Climbing 調節)。客人少時,廚房有些廚師會先回家休息(釋放執行緒,避免浪費資源)。
✅ 例如 Task.Run 就是告訴櫃台「這道菜需要人現場煮,丟給後廚去做」。
🎵 I/O Completion Threads = 外送取餐通知
有些菜是不用自己煮的,比如外包餐點(非同步 I/O),櫃台下單後是外面的合作店家準備。餐點做好後,不需要廚師跑去取餐,而是外送通知來了(OS 通知),專門的通知員(I/O Completion Thread)負責回報「外送到了」。廚師不需要卡著等外送,省下人力。
✅ 例如 HttpClient、FileStream.ReadAsync,背後都是 OS 幫你監聽,準備好再通知,不佔用廚師(Worker Thread)。
🎵 Custom Thread = 老闆找來的私廚
有些老闆想要做特殊料理(例如長期監控、背景資料同步),就會另外請一個私廚(new Thread())。這位私廚不屬於餐廳原本的後廚團隊(ThreadPool),所以沒辦法自動被餐廳調度。你要自己負責這位私廚什麼時候上班、什麼時候休息,還要自己付錢,請多了還會佔掉廚房空間。
✅ 通常只有少數需要長期跑、不想被 ThreadPool 管理的情境才會用到。
類型 | 角色 | 特點 |
---|---|---|
Main Thread (主執行緒) | 譬如 ASP.NET 的 Request 處理執行緒、桌面程式的 UI 執行緒 |
不能被卡太久,否則 UI 畫面卡住、Web Request 超時 |
Worker Threads | ThreadPool 的主要執行緒,跑 Task.Run 、QueueUserWorkItem |
處理 CPU Bound 的工作或短時間非同步任務 |
I/O Completion Threads | 專門處理非同步 I/O 的回呼 | 像 File IO、Socket、HttpClient 用 OS 的非同步 API,完成後這邊回呼 |
Custom Thread | 你自己 new Thread() 建的獨立執行緒 |
不進 ThreadPool,不受 ThreadPool 管理,成本高,一般很少需要 |
🎵 「人力底線」與「人力上限」
在這間 24 小時營業的智慧餐廳裡,負責烹調訂單的就是「後廚」——也就是執行緒集區(ThreadPool)。
即使廚房裡的工作(任務)數量會忽高忽低,老闆(.NET Runtime)還是得先準備好人力調度的兩條底線:
- 1️⃣ 人力底線(GetMinThreads)
- 2️⃣ 人力上限(GetMaxThreads)
🍳 後廚人力底線 — GetMinThreads
ThreadPool.GetMinThreads(out workerThreads, out completionPortThreads)
這個設定就像是規定「後廚最少要留幾位廚師 standby」。就算此刻沒有新訂單進來,也要先留住這些廚師,確保當訂單瞬間湧入時,不會因為重新招人(建立執行緒)而延誤上菜(執行任務)。
在 GetMinThreads 中有兩個維度:
- workerThreads:CPU Bound 工作(純運算)
- completionPortThreads:I/O Bound 工作(非同步 I/O)
1 |
|
這代表:
這台機器預設至少會有 20 個背景工作執行緒隨時待命處理 CPU 密集任務,以及至少 1 個 I/O 完成執行緒,用於非同步 I/O 事件的回呼,這個最小值通常跟 CPU 核心數有關(包含超執行緒 Hyper-Threading)。
目的在於降低執行緒從無到有的建立成本,換取在高峰期能即時出菜。
🍳 2️⃣ 後廚人力上限 — GetMaxThreads
ThreadPool.GetMaxThreads(out workerThreads, out completionPortThreads)
另一個極限就是「最多能請多少臨時工進廚房幫忙」,也就是執行緒集區能夠擴充到的最大執行緒數量。如果有大量訂單同時進來,後廚就會動態招募更多廚師,但絕不會超過這個人力上限,以免廚房人太多彼此擠到走不開,反而降低效率、耗盡系統資源。
1 |
|
ThreadPool 最多可以容納 32,767 個 CPU Bound 執行緒,最多可容納 1,000 個 I/O Completion 執行緒
總結是這樣
- 最小值太低 ➜ 容易接單卡住,導致吞吐量下降
- 最小值太高 ➜ 閒置執行緒也會佔用資源
- 最大值太低 ➜ 並發能力受限
- 最大值太高 ➜ 太多執行緒反而彼此爭奪 CPU,Context Switching 開銷反噬效能
🎵 ThreadPool 小實驗
在一個應用程式(Process)裡,執行程式碼的最小單位就是 執行緒(Thread)。
多執行緒(Multi-Thread)就是指:同一時間內,這個應用程式裡有多個執行緒在同時執行不同的工作。
在 .NET 中,你可以自己建立 Thread,或是更常見的是把工作丟給 ThreadPool(執行緒集區)自動幫你安排哪位廚師來做,省去手動管理執行緒的麻煩。
1 |
|
- 1️⃣ 先輸出開始時的 Thread ID,通常是 1,代表這是主執行緒(Main Thread)。
- 2️⃣ 呼叫 Task.Run,它會把工作排進 ThreadPool,呼叫點(Main Thread)不會被卡住你的背景工作會在別的執行緒執行
- 3️⃣ 馬上輸出結束時的 Thread ID,可以看到開始和結束的 Thread ID 相同,證明主執行緒執行到這裡沒有被阻塞。
start 和 end 仍然是同一個 Thread ID,證明 Main Thread 自己沒有被阻塞。
🎵 結語
在這座非同步餐廳裡,從櫃台接待(Main Thread)到後廚大廚(Worker Threads),從外送取餐通知(I/O Completion Threads)到偶爾請來的私廚(Custom Thread),所有執行緒都像季節般輪替,繁忙與清閒、擴充與回收,都依照需求流轉。
人力底線與上限提醒我們:資源永遠有限,調度若不精準,就會卡住整條服務流程,而過度揮霍,又可能讓系統背負不必要的負擔。
也許,程式裡沒有真正「永遠待命的廚房」,但透過 ThreadPool、Task 和非同步 I/O,我們得以讓每位廚師在對的時間端出最熱騰騰的料理,用最少的浪費,換取最大的效率。
執行緒輪迴,四季更替,當你理解背後的調度邏輯,就能在這片非同步之林裡,靈活調度人力、掌握資源,把每一次請求,都變成一道最合時宜的好菜。