連載《深入淺出 .NET 10 非同步程式設計》,這是第 1 章的完整內容。
作為起手式的第一章,我們會盡量用通俗易懂的方式來展開。無論你之前有沒有接觸過執行緒和非同步程式設計,都能先建立起清楚的核心概念。
為何程式需要同時處理多件事?
你是否曾經遇過這種情況:在桌面應用程式中按下一個按鈕後,整個視窗變成一片空白,滑鼠游標一直轉圈圈,完全無法操作?或者,在瀏覽某個熱門購物網站時,網頁載入速度極慢,甚至直接顯示逾時錯誤?
這些惱人的體驗,往往指向同一個核心問題:軟體沒有有效地處理「等待」。無論是等待檔案儲存、等待網路回應,或是等待資料庫查詢結果,這些等待如果處理不當,就會導致程式卡頓,使用者體驗變差。
在數位時代,我們期待應用程式能提供即時回饋與流暢體驗。無論是要讓桌面應用程式的使用者介面(UI)保持靈敏,還是要讓網站伺服器同時服務成千上萬名使用者,背後都仰賴同一種能力:同時處理多項任務。若缺乏這種能力,應用程式的反應就會變得遲鈍,使用者體驗會大打折扣,伺服器也無法應付現代網路的龐大流量。
為了把這項能力看得更具體一點,我們先用披薩店當作類比,作為這趟學習旅程的起點。接下來,請想像我們要一起經營一家披薩店,從最原始的營運模式開始,逐步探索如何提升效率。透過這個類比,我們會慢慢理解 C# 程式設計中的兩個核心概念:多執行緒(multithreading) 和 非同步程式設計(asynchronous programming)。
本章學習路徑
接下來,這趟披薩店學習旅程會依序經過三個模式:
- 模式一:同步的單一廚師 我們將從最基本的單執行緒模式開始,了解其運作方式與效率瓶頸。
- 模式二:更靈活的多工廚師 接著探索多執行緒如何改善效率,以及它所帶來的新挑戰。
- 模式三:超級高效的智慧廚師 最終,我們將揭曉非同步程式設計的威力,以及 C# 如何透過
async/await讓這一切變得自然又簡單。
準備好了之後,我們就從這家披薩店最原始的經營模式開始,看看第一位廚師是如何工作的。
模式一:同步的單一廚師
關鍵概念: 單執行緒、阻塞
在披薩店剛開始營運時,店裡只有一位廚師。這位廚師就等同於程式世界中的 執行緒(thread),也就是執行所有工作的唯一單位。他的工作流程非常簡單:接到訂單、準備餅皮、放上配料、送入烤箱,然後等披薩烤好再取出。
這個模式被稱為 同步單一執行緒(synchronous single-threading)。這裡「同步」的意思是:所有步驟都必須依序完成,一步接著一步。這裡的關鍵問題是「等待」:當廚師把披薩放進烤箱後,他哪兒也去不了,只能站在原地盯著烤箱,直到計時器響起。在這段時間裡,他完全無法處理新訂單,也不能先準備下一個披薩。
為了更有感地看見這種卡住的節奏,我們先來看一段簡單的程式。這段程式展示的,就是單一廚師(單一執行緒)的工作流程:
// 模式一:同步的單一廚師 (Synchronous Single-Threading)
// 負責展示單執行緒阻塞的現象
using System.Diagnostics;
Console.WriteLine("披薩店開始營業!模式一:只有一位廚師 (單執行緒)");
var sw = Stopwatch.StartNew();
// 模擬依序製作三個披薩
MakePizza(1);
MakePizza(2);
MakePizza(3);
sw.Stop();
Console.WriteLine($"所有披薩製作完成,總共耗時: {sw.ElapsedMilliseconds} 毫秒");
void MakePizza(int id)
{
Console.WriteLine($"[單一廚師] 開始準備第 {id} 份披薩的餅皮...");
Thread.Sleep(500); // 模擬切菜和揉麵的準備時間
Console.WriteLine($"[單一廚師] 將第 {id} 份披薩送入烤箱,開始等待...");
// 這裡使用 Thread.Sleep 來模擬「阻塞操作 (blocking operation)」
// 廚師在此期間什麼都不能做,只能死等烤箱完成,既無法準備下一份,也無法接聽電話
Thread.Sleep(2000);
Console.WriteLine($"[單一廚師] 第 {id} 份披薩烤好了!取出披薩。");
}執行結果:
披薩店開始營業!模式一:只有一位廚師 (單執行緒)
[單一廚師] 開始準備第 1 份披薩的餅皮...
[單一廚師] 將第 1 份披薩送入烤箱,開始等待...
[單一廚師] 第 1 份披薩烤好了!取出披薩。
[單一廚師] 開始準備第 2 份披薩的餅皮...
[單一廚師] 將第 2 份披薩送入烤箱,開始等待...
[單一廚師] 第 2 份披薩烤好了!取出披薩。
[單一廚師] 開始準備第 3 份披薩的餅皮...
[單一廚師] 將第 3 份披薩送入烤箱,開始等待...
[單一廚師] 第 3 份披薩烤好了!取出披薩。
所有披薩製作完成,總共耗時: 7557 毫秒原始碼: DemoSingleThread
核心概念
讀到這裡,我們先暫停一下,把剛剛出現的幾個核心概念整理清楚,再往下一節走。
- 處理序 (process) 是應用程式的「容器」(或沙盒),它擁有獨立的記憶體空間和系統資源,確保應用程式之間不會互相干擾。
- 執行緒 (thread) 則是容器中實際負責執行程式碼的「工人」。
一個處理序至少會有一位工人,也就是主執行緒。但如果要同時處理多項工作,同一個處理序也可以擁有多位工人,也就是多執行緒。這些工人共享同一個容器內的資源,例如記憶體;也正因如此,多執行緒程式設計才需要特別注意資源競爭的問題。
效率瓶頸:等待的代價
接著,我們就能更明白這種模式最大的問題:極度浪費資源。廚師代表的是寶貴的 CPU 運算能力,但他大部分時間都耗在無謂的等待上。雖然烤箱正在努力工作,廚師本人卻完全閒置。
這個流程雖然簡單,也容易理解,但效率非常低。想像一下,在尖峰時段顧客大排長龍,我們的廚師卻因為正在等一個披薩烘烤,完全無法處理任何新訂單。放到軟體世界裡,這就是單執行緒阻塞模型最典型的效能瓶頸。
程式碼中的「阻塞」範例
說到這裡,先暫時離開披薩店一下,看看這種「等待」在實際程式碼中會長成什麼樣子,以及它到底會造成什麼後果。
以桌面應用程式(如 WinForms、WPF)為例,這類程式通常只有一條「UI 執行緒」負責處理使用者操作,例如點擊按鈕、移動視窗等等。如果我們在這條執行緒上執行耗時操作,整個程式看起來就會像「當掉」一樣。下面這段程式正是這種情況:
private void SaveButton_Click(object sender, EventArgs e)
{
// Thread.Sleep() 用來模擬一個耗時 5 秒的儲存操作。
// 在這段時間內,UI 執行緒會被完全卡住,無法回應任何使用者操作。
// 使用者會發現視窗無法移動、按鈕沒有回應,甚至滑鼠游標變成轉圈圈。
Thread.Sleep(5000);
MessageBox.Show("檔案已儲存!");
}既然等待這麼浪費時間,下一步自然就是想辦法提升效率。
模式二:引進更多人手
關鍵概念: 多執行緒與併發(concurrency)
為了處理這種等待造成的低效率,我們替廚房引進了新的運作模式:多執行緒(multithreading)。背後的想法很直觀:與其讓一位廚師乾等到整個廚房都卡住,不如安排更多廚師分工,讓某些工作在等待時,其他工作仍然有人可以繼續推進。
歷史上,這確實是很常見的思路。不過,這裡先記住一件事:如果等待的本質是檔案、網路或資料庫這類 I/O 操作,現代 .NET 通常更傾向使用非同步 I/O,而不是為每一個等待中的工作綁定一條額外執行緒。這一節先把多執行緒的基本觀念建立起來,下一節再來看更適合 I/O 等待的作法。
先看最容易理解的情況。在只有單一 CPU 核心(core)的電腦上,這就像是我們請了多位廚師,但廚房裡只有一個工作台。這些廚師必須頻繁地輪流使用工作台來做事。從外部看起來像是在「同時」處理多張訂單,但實際上在任何一個瞬間,只有一位廚師佔用工作台,靠著快速切換創造出同時進行的「錯覺」。
如果換成擁有多個 CPU 核心的現代電腦,情況就更進一步了。這相當於我們真的多請了幾位廚師。此時,多位廚師可以真正同時在各自的工作台上製作披薩,實現真正的平行處理(parallelism)。
併發 vs. 平行
在繼續往下看之前,先補一個很容易混在一起的觀念:併發和「平行」不是同一件事。
- 併發(concurrency):指管理多個任務的能力。就像披薩店中只有一位廚師在多個任務(揉麵、放料、等烤箱)之間來回切換。任務在時間上是重疊的,但不一定在同一時刻被執行。這主要跟結構的設計有關。
- 平行(parallelism):指同時執行多個任務。在電腦世界裡,這需要多個 CPU 核心來實現。在我們的披薩店裡,這相當於我們真的多請了幾位廚師(多個執行緒),每個人都在自己的工作台(多個 CPU 核心)上同時製作披薩。這主要是跟執行的方式有關。
多執行緒既能實現單核心上的併發(concurrency),也能利用多核心實現平行(parallelism)。
有了這個區別之後,再來看程式碼就會順很多。下面這個範例展示的,就是多位廚師(多執行緒)的工作流程:
// 模式二:更靈活的多工廚師 (multithreading)
// 負責展示多執行緒併發處理的現象
using System.Diagnostics;
Console.WriteLine("披薩店開始營業!模式二:有多位廚師同時工作 (多執行緒)");
var sw = Stopwatch.StartNew();
// 建立三個獨立的執行緒(分別代表三位不同的廚師)
Thread chef1 = new Thread(() => MakePizza(1));
Thread chef2 = new Thread(() => MakePizza(2));
Thread chef3 = new Thread(() => MakePizza(3));
// 讓所有廚師同時開始工作
chef1.Start();
chef2.Start();
chef3.Start();
// 主執行緒(餐廳經理)等待所有廚師完成工作
chef1.Join();
chef2.Join();
chef3.Join();
sw.Stop();
Console.WriteLine($"所有披薩製作完成,總共耗時: {sw.ElapsedMilliseconds} 毫秒");
void MakePizza(int id)
{
int threadId = Environment.CurrentManagedThreadId;
Console.WriteLine($"[廚師 {threadId}] 開始準備第 {id} 份披薩的餅皮...");
Thread.Sleep(500); // 模擬切菜和揉麵的準備時間
Console.WriteLine($"[廚師 {threadId}] 將第 {id} 份披薩送入烤箱,開始等待...");
// Thread.Sleep 是阻塞操作,但因為每個披薩都有專屬的廚師(執行緒),
// 某個廚師在等待時,其他的廚師仍然可以在自己的工作台上處理其他的披薩。
Thread.Sleep(2000);
Console.WriteLine($"[廚師 {threadId}] 第 {id} 份披薩烤好了!取出披薩。");
}執行結果(輸出順序、Thread ID 與耗時都可能會依執行環境與排程而略有不同):
披薩店開始營業!模式二:有多位廚師同時工作 (多執行緒)
[廚師 4] 開始準備第 2 份披薩的餅皮...
[廚師 5] 開始準備第 3 份披薩的餅皮...
[廚師 3] 開始準備第 1 份披薩的餅皮...
[廚師 4] 將第 2 份披薩送入烤箱,開始等待...
[廚師 5] 將第 3 份披薩送入烤箱,開始等待...
[廚師 3] 將第 1 份披薩送入烤箱,開始等待...
[廚師 4] 第 2 份披薩烤好了!取出披薩。
[廚師 3] 第 1 份披薩烤好了!取出披薩。
[廚師 5] 第 3 份披薩烤好了!取出披薩。
所有披薩製作完成,總共耗時: 2548 毫秒原始碼: DemoMultiThread
多執行緒的利與弊
這種模式確實帶來了顯著改善,但也同時引入了新的複雜性。它最主要的優點,是提升 CPU 利用率。也就是說,廚師(CPU)的閒置等待時間明顯減少了。無論是在任務間切換,還是在多核心上同時工作,都能讓整體出餐效率提升不少。
缺點則有:
context switch的開銷:廚師在不同任務間切換並不是零成本。他得先放下處理 A 披薩的工具、洗手,再拿起處理 B 披薩的工具。這個「切換」本身就會消耗時間和精力。在程式中,作業系統切換執行緒也需要保存當前狀態、載入新狀態,這同樣會帶來效能開銷。- 資源同步問題(synchronization issues):例如,多位廚師如果想同時使用唯一的一把醬料勺,就會發生衝突。這在程式中會引發 競爭狀況(race conditions)。更麻煩的是,還可能發生 死結(deadlocks)。想像一下,廚師 A 拿了唯一的醬料勺,正在等待廚師 B 用完唯一的起司刨絲器;但同時,廚師 B 卻拿著刨絲器,正在等待廚師 A 交出醬料勺。兩位廚師都卡住了,互相等著對方,程式也就跟著完全卡死。
執行緒的隱性成本
在 .NET 的記憶體回收過程中,某些階段會需要暫停受控執行緒(managed threads);執行緒越多,協調與恢復的成本通常也越高。不過,這並不表示每一次 GC 都會從頭到尾完全停住所有執行緒:現代 .NET 的 background GC 會讓應用程式執行緒在大部分時間繼續執行,只在特定階段短暫暫停。同樣地,除錯器在命中中斷點時,也往往會暫停該應用程式的其他執行緒,直到你繼續執行。
參閱微軟文件:Garbage collection and performance、Background garbage collection
看到這裡,你大概已經感覺到了:多執行緒雖然提升了效率,但管理上的複雜性也跟著上來。那麼,有沒有一種更優雅、更高效的方式,讓廚師不用這麼手忙腳亂,同時又能把時間利用到極致呢?
模式三:超級高效的廚師
關鍵概念: 非同步程式設計
接下來,我們把視角再往前推一步。現在,披薩店迎來了一位更聰明的「超級廚師」,而他的工作模式就和 非同步程式設計(asynchronous programming) 極為相似。
當這位廚師把披薩放進烤箱後,他既不是原地等待,也不是立刻轉身去做另一個披薩。他做了另一種選擇:把「烤披薩」這件耗時的工作完全委託給烤箱,然後徹底釋放自己,去做不同類型的事情,例如到前台接聽電話訂單或服務顧客。他之所以能這麼做,是因為烤箱被設定成烤好後會自動發出「叮」的一聲,也就是 回呼通知(callback)。等他聽到通知聲,再暫停手邊的工作,回來處理烤好的披薩就行了。
接著,我們就用程式碼把這個流程具體看一次。以下範例展示的,就是這位超級廚師(非同步)的工作方式。
Note
在以下範例中,你會先看見
async、await和Task這些陌生的關鍵字。先別擔心,也不用急著弄懂每個細節。這一輪請先專注觀察:等待期間,執行緒(廚師)是不是真的被釋放了。也先記住一點:async/await擅長的是在「等待 I/O」時不阻塞執行緒,而不是把 CPU 工作自動變快。
// 模式三:超級高效的智慧廚師 (Asynchronous Programming)
// 負責展示使用 async/await 釋放執行緒,達成非阻塞的等待
using System.Diagnostics;
var msg = "披薩店開始營業!模式三:超級廚師搭配智慧烤箱 (非同步程式設計)";
Console.WriteLine(msg);
var sw = Stopwatch.StartNew();
// 啟動三個非同步的披薩製作任務(它們會併發執行非同步邏輯)
var p1 = MakePizzaAsync(1);
var p2 = MakePizzaAsync(2);
var p3 = MakePizzaAsync(3);
// 非同步等待所有披薩任務完成
await Task.WhenAll(p1, p2, p3);
sw.Stop();
msg = $"所有披薩製作完成,總共耗時: {sw.ElapsedMilliseconds} 毫秒";
Console.WriteLine(msg);
async Task MakePizzaAsync(int id)
{
// 獲取目前執行緒的 ID,觀察非同步的執行緒變化
int threadId = Environment.CurrentManagedThreadId;
var str = $"[廚師 {threadId}] 開始處理第 {id} 份披薩,先等待麵糰醒發...";
Console.WriteLine(str);
// 模擬一段可非同步等待的前置時間,例如等待麵糰醒發或配料送達:不阻塞執行緒
await Task.Delay(500);
threadId = Environment.CurrentManagedThreadId;
str = $"[廚師 {threadId}] 麵糰準備好了,將第 {id} 份披薩送入烤箱,設定計時器後即去處理其他事情!";
Console.WriteLine(str);
// 這裡用 Task.Delay 來模擬網路連線、讀寫檔案這類需要等待外部回應的
// 耗時操作(即 I/O 密集型操作),例如此處的烤箱烘烤。
// 關鍵點:執行緒在這裡被「完全釋放」了,它不會被阻塞。系統可以將該執行緒
// 派去處理其他任務,直到烤箱時間到了再透過系統的排程繼續執行下一行程式。
await Task.Delay(2000);
threadId = Environment.CurrentManagedThreadId;
str = $"[廚師 {threadId}] 「叮!」第 {id} 份披薩烤好了,廚師回來取出披薩。";
Console.WriteLine(str);
}執行結果(輸出順序、Thread ID 與耗時都可能會依執行環境與排程而略有不同):
披薩店開始營業!模式三:超級廚師搭配智慧烤箱 (非同步程式設計)
[廚師 2] 開始處理第 1 份披薩,先等待麵糰醒發...
[廚師 2] 開始處理第 2 份披薩,先等待麵糰醒發...
[廚師 2] 開始處理第 3 份披薩,先等待麵糰醒發...
[廚師 5] 麵糰準備好了,將第 3 份披薩送入烤箱,設定計時器後即去處理其他事情!
[廚師 7] 麵糰準備好了,將第 1 份披薩送入烤箱,設定計時器後即去處理其他事情!
[廚師 8] 麵糰準備好了,將第 2 份披薩送入烤箱,設定計時器後即去處理其他事情!
[廚師 7] 「叮!」第 1 份披薩烤好了,廚師回來取出披薩。
[廚師 5] 「叮!」第 2 份披薩烤好了,廚師回來取出披薩。
[廚師 8] 「叮!」第 3 份披薩烤好了,廚師回來取出披薩。
所有披薩製作完成,總共耗時: 2530 毫秒原始碼: DemoAsyncAwait
為什麼廚師編號會變來變去?
仔細觀察上面的執行結果,你會發現同一個披薩在不同階段,負責處理的廚師編號(執行緒 ID)改變了!例如第 1 份披薩原本是 2 號廚師開始處理,等待麵糰醒發後,卻變成 7 號廚師接手將它放進烤箱。
這是因為在主控台應用程式(Console App)中,方法執行到 await 處(例如等待麵糰或烤箱時)會先暫停,並把控制權交還出去,不再佔用目前那條執行緒。等到等待結束後,後續步驟通常會由執行緒池(ThreadPool)中任何一條剛好有空的執行緒接手執行,所以你才會看到執行緒 ID 改變。這正是非同步極具彈性的地方:工作不會綁死在特定一條執行緒上。如果是在具有 UI 執行緒的桌面程式中,後續步驟通常會回到原本的 UI context,因此看起來往往像是由同一位專屬廚師接手到底,這點我們會在後面的章節詳細探討。
非同步的關鍵:不阻塞執行緒
看到這裡,可以把焦點拉回來了。非同步與多執行緒的根本區別,就在於它們如何使用 執行緒(廚師):
- 多執行緒(模式二):廚師(執行緒)始終被佔用在廚房裡。他利用等待時間,在多個披薩之間瘋狂切換,但他從未離開廚房。
- 非同步(模式三):廚師(執行緒)將等待的工作交給烤箱後,就完全被釋放了。他可以離開廚房去前台接電話(執行緒可以暫時離開,去處理其他完全無關的請求)。
換句話說,非同步模式是在極大化利用廚師(執行緒)的時間。當執行緒遇到需要等待的 I/O 操作,例如檔案讀寫或網路請求時,它不會被「阻塞」在原地,而是可以被系統調度去處理其他任務。這樣一來,應用程式的吞吐量(throughput)和反應速度通常都會更好。
不過,這裡有一個很重要的界線要先記住:所謂「釋放執行緒」,是指在等待 I/O 或計時器期間,不把執行緒佔住。若工作本身是實際的 CPU 計算,例如影像處理、加密或資料轉換,async/await 並不會憑空把它變成非阻塞。那類工作仍然需要佔用 CPU,只是通常會搭配 Task.Run、Parallel 或其他平行化工具來處理。
傳統非同步的挑戰:混亂的筆記本
關鍵概念: 回呼地獄(callback hell)
不過,故事還沒結束。在 C# 的 async/await 語法出現之前,要實現這種高效的非同步模式其實非常困難。那就像要求我們的超級廚師隨身攜帶一本極其複雜的筆記本,上面寫滿各種「if … then …」的指令:
「如果電話響了,就記錄下訂單資訊,然後去看筆記本的第 5 頁。」 「如果烤箱響了,就放下電話,去取出披薩,然後去看筆記本的第 8 頁。」 「如果送餐員回來了,就……」
這種基於回呼(callback)的程式碼,邏輯流程會被分割得支離破碎,形成所謂的「回呼地獄(callback hell)」,導致程式碼難以閱讀、除錯和維護。
幸運的是,C# 後來提供了一本「魔法食譜」,讓這位超級廚師的工作流程終於變得清楚許多。
魔法食譜:async 與 await 的威力
為了解決「回呼地獄」帶來的混亂,C# 引入了 async 和 await 這兩個關鍵字。我們可以把它們想像成一本「魔法食譜」。有了這本食譜,廚師就能用看似簡單、線性的同步方式,去閱讀和執行複雜的非同步流程,等於正式告別了那本混亂的筆記本。
async 與 await 如何運作
接下來,我們把鏡頭拉近一點,看看這本魔法食譜最核心的兩個關鍵字:
async關鍵字- 比喻:這就像是在食譜的封面上標註「魔法食譜」。這個標記本身不會改變任何事,但它至關重要。
- 作用:它有兩個核心目的。第一,允許在方法內使用
await關鍵字。第二,它會指示編譯器將整個方法轉換為一個精密的狀態機(state machine),以便在幕後管理複雜的非同步流程。這涉及較底層的編譯器魔法,我們會在後續的進階章節中再揭開它的神秘面紗。現階段只要先把它想像成一個會自動幫我們記住「目前做到哪一個步驟了」的機制即可。
await關鍵字- 比喻:這是魔法食譜中最關鍵的指令。當廚師讀到
await oven.BakeAsync()這一行時,他會先看看烤箱工作是否已完成。如果還沒完成,他就把控制權交還,先去做其他完全無關的工作,例如服務其他顧客,而不是站在原地傻等;如果工作其實已經完成,就能直接進入下一步。 - 作用:
await會先檢查它等待的Task是否已完成。若尚未完成,方法就會先暫停。等到烤箱,也就是 I/O 操作完成後,再由編譯器產生的狀態機安排後續步驟。這在非同步程式設計中稱為接續工作(continuation)。若工作已完成,則通常會直接往下執行。也正因如此,整個流程看起來像同步寫法,卻能在等待期間避免阻塞執行緒。
幕後功臣:Task 物件
看到這裡,你可能會自然冒出一個問題:await 到底在「等待」什麼呢?答案就是 Task 物件。
Task的比喻:當廚師執行一個非同步操作時(如BakeAsync()),他不會立刻得到一個披薩,而是會得到一張「訂單收據」。這張收據就是Task物件,它代表一個「未來會完成的工作」(正式的術語叫做 future 或 promise)。Task<T>的比喻:如果這個工作完成後會返回一個結果(例如一個烤好的披薩),那麼廚師得到的就是一張可以兌換披薩的收據,這就是Task<Pizza>。await的作用:await的核心作用,就是非阻塞地等待這張「收據」被兌現。你可以把它想像成廚師把收據交給系統,然後轉身去做別的事。系統的「魔法」在於,當訂單完成時,它會輕拍廚師的肩膀,提醒他回到原本離開的地方繼續工作。如果等待的是一張Task<Pizza>收據,那麼await的過程,就是最終從收據中「拆開」並取出那個熱騰騰的Pizza。
看到這裡,async/await 的魔力應該就比較清楚了。它讓我們能用同步、線性的思維方式來編寫程式碼,同時又享受到非同步程式設計帶來的好處,堪稱 C# 中最好用的解決方案之一。
總結:選擇最適合的模式
從只有一位廚師的簡陋小店,到運用智慧科技的高效餐廳,我們一路看見了一間披薩店,也就是應用程式,如何逐步演進。這裡用一張表格把這三種營運模式整理一下:
要提醒的是,這張表描述的是常見傾向,而不是放諸四海皆準的絕對規則;真正該選哪種模式,還是要看瓶頸是在 CPU 還是 I/O。
下一步
透過經營一家披薩店,我們逐步認識了最基本的阻塞模型,以及更高效的非同步模型。這一章最想先建立的觀念是:async/await 是現代 C# 開發者處理耗時操作,特別是 I/O 操作時的首選工具。它幾乎不會大幅增加程式碼複雜度,卻能明顯提升應用程式的效能和反應速度,讓你的應用程式像那位「超級廚師」一樣,自然又高效地處理各種任務。
如果這個披薩店的類比有幫你把基礎觀念建立起來,那麼這一章的任務就達成了一大半。接下來,就能以此為起點,繼續探索更進階的主題,例如 Parallel 類別、PLINQ,以及更複雜的同步機制,逐步打造出真正高效、可靠的現代化應用程式。
本文摘自 《深入淺出 .NET 10 非同步程式設計》的第 1 章完整內容。
沒有留言: