1. 介紹
阻塞呼叫— 任何會「阻塞」執行緒直到某個操作完成的呼叫。通常是:
- 讀取或寫入資料(例如從磁碟)。
- 長時間的資料庫或網路請求。
- 呼叫第三方函式庫,它執行得很「慢」。
在這些時刻,執行緒只是站著、踏步、等待:「怎麼樣了,何時才有回應?」
生活中的例子
// 你的主要邏輯
Console.WriteLine("等待來自伺服器的回應...");
string response = CallServer(); // 這裡整個卡住了!
Console.WriteLine("伺服器回應: " + response);
在伺服器回應之前,你的執行緒「卡住」— 什麼都做不了!
在應用程式中長什麼樣子
在不同類型的應用中表現不同。在主控台程式看起來就像「凍住」了 — 游標不再閃爍,沒有任何反應。在 GUI(例如 WinForms 或 WPF)中,視窗會突然停止回應,出現熟悉的 “not responding”,使用者可能會對你的軟體和你的人生觀表示不滿。在伺服器應用中情況更糟:一個卡住的執行緒意味著少了一個可服務的使用者。
2. 在 C# 中實作的阻塞呼叫
我們用教學專案「魔法球」手把手感受問題。假設有如下片段:
Console.WriteLine("請輸入你要問魔法球的問題:");
string question = Console.ReadLine(); // 阻塞呼叫:等使用者輸入!
Console.WriteLine("在想答案...");
Thread.Sleep(5000); // 模擬長時間工作
Console.WriteLine("答案:請明天再問!");
- Console.ReadLine() — 會一直等使用者輸入(阻塞執行緒)。
- Thread.Sleep(5000) — 人為造成暫停,凍結執行緒。
問題: 如果我們只是寫主控台程式,這有什麼不好?
回答: 在主控台程式不那麼致命 — 使用者自己會等。但如果不是單一使用者呢?假如是伺服器每秒來 1000 個請求?或是 GUI 程式,使用者期待介面立即回應,而不是你的哲學沉思?
範例:阻塞 UI
// 在 WinForms 的按鈕處理器:
private void button1_Click(object sender, EventArgs e)
{
label1.Text = "載入中...";
DoHeavyWork(); // 重的操作(例如 DB 查詢)
label1.Text = "完成!";
}
問題: 當 DoHeavyWork 執行時,視窗無法更新 UI,對鍵盤與滑鼠不回應,也不會重繪。如果使用者試著關閉視窗 — Windows 會顯示「未回應」並建議結束程序。
3. 多執行緒世界的核心痛點
阻塞呼叫的問題
- 資源浪費。 每個 .NET 的 Thread 都是相對「重」的物件。作業系統會為它分配 stack(通常 1 MB)、描述符、同步物件等。如果執行緒「閒置」(在等檔案或網路),它在「無所事事」的同時還佔用記憶體。
- 執行緒有限。 伺服器應用通常有一個 thread pool。如果所有執行緒都被阻塞 — 新的請求無法處理。例如在 ASP.NET:當所有可用執行緒都在等資料庫回應時,網站對新使用者會「凍結」。
- 介面回應差。 在 GUI 應用中,UI 執行緒被阻塞時,視窗連「關閉」都會變得沒反應。
- 效能下降。 被阻塞的執行緒越多,context switch 越多,作業系統負擔越重,整台機器越慢。
常見的阻塞來源
來看一下會在「平常情況下」阻塞執行緒的那些呼叫:
- 網路操作:呼叫 API、下載檔案、更新檢查。
- 磁碟/檔案系統:讀寫大檔案。
- 資料庫:長時間的 SQL 查詢或本地資料庫存取。
- 使用者輸入:像是耗時的 ReadLine — 看似小事但會阻塞。
- Thread.Sleep, Task.Delay:會人為「凍結」執行緒。
範例(從網站載入資料)
using System.Net.Http;
HttpClient client = new HttpClient();
string result = client.GetStringAsync("https://google.com").Result; // 阻塞的同步呼叫!
Console.WriteLine(result);
發生了什麼?方法 .Result 會阻塞執行緒直到拿到回應!
4. 如何判斷你的呼叫會不會阻塞執行緒?
很簡單:如果方法在它完成之前不把控制權還回來(在等候、輸入、網路、磁碟),那它就是阻塞呼叫。
- 帶有 Thread.Sleep, Task.Wait, .Result, .Wait() 的方法會阻塞。
- 在沒有非同步版本的情況下出現的讀寫方法也會阻塞。
- 「掛起」的視窗或停頓的伺服器通常是徵兆。
視覺化:阻塞呼叫的流程圖
+-------------------+
| 啟動方法 |
+-------------------+
|
v
+-------------------+
| 呼叫阻塞方法 |
| (例如,讀檔案) |
+-------------------+
|
v
+-------------------+
| 等待操作完成 |
+-------------------+
|
v
+-------------------+
| 方法回傳並繼續執行 |
+-------------------+
5. 為何阻塞呼叫在伺服器應用特別糟糕
大多數現代應用都是網路或伺服器型的。即便你寫遊戲,也會從伺服器載入內容。做網站時,一定會跟網路和磁碟打交道。
想像你的伺服器每秒處理 1000 個請求。每個請求都需要和資料庫溝通。你寫了:
// 網路處理器
string data = db.ReadDataSync(); // 同步阻塞!
return new Response(data);
在資料庫查詢期間,執行緒只是閒著等。單個請求沒事,但當請求堆積時,所有執行緒都被占滿。新的請求無法取得執行緒,只能在隊列裡等。最後伺服器變成一條長長的隊伍,大家都站著、打哈欠、等有人先動。
示意:「同步處理請求」
請求 1: 搶到執行緒 -> 等資料庫 -> 釋放
請求 2: 搶到執行緒 -> 等資料庫 -> 釋放
...
請求 100: 沒有空執行緒,排隊等候...
6. 非同步(asynchronous)— 對抗阻塞的解藥
為了避免阻塞,我們用 非同步呼叫。在 C# 裡這兩個關鍵字很重要:async 和 await。
它們可以做到:
- 不阻塞執行緒(特別是寶貴的 UI 或伺服器執行緒)。
- 避免無謂地佔用資源。
- 改善 GUI 的回應性。
- 不會在慢操作進行時「佔著」執行緒。
注意: 我們不建議一開始就盲目使用非同步,因為它需要理解執行緒與阻塞。現在你已經理解了阻塞帶來的痛苦,非同步的方式會讓你和使用者都爽很多。
表格:什麼會阻塞,什麼不會?
| 方法/呼叫 | 會阻塞執行緒嗎 | 適合 UI/Server 嗎 |
|---|---|---|
|
會 | 不適合 |
|
會 | 不適合 |
|
會 | 不適合 |
|
會 | 不適合 |
|
會 | 不適合 |
|
不會 | 適合 |
|
不會 | 適合 |
|
不會 | 適合 |
備註: await 只能在非同步函式中使用(例如 async Task ...),我們會在接下來的課程詳細講解。
7. 與阻塞呼叫工作時常見的致命錯誤
「阻塞 UI — 使用者罵翻所有東西」
private void btnLoad_Click(object sender, EventArgs e)
{
var data = BigFileReader.Read(@"C:\huge.dat"); // 阻塞呼叫!
textBox1.Text = data;
}
在巨大的檔案讀完之前,視窗會「掛掉」。
「伺服器卡住 — 客戶跑光了」
public IActionResult Download()
{
var content = File.ReadAllBytes("bigfile.zip"); // 同步、耗時、阻塞 ASP.NET 的執行緒!
return File(content, "application/zip");
}
小型伺服器(例如免費主機)可能會因為這種做法直接「崩潰」。
「非同步方法 + .Wait()/.Result = 同步自殺」
public void LoadData()
{
var result = DoAsyncWork().Result; // 阻塞執行緒!
}
.Result 和 .Wait() 會把即便寫得很漂亮的非同步程式碼變回普通的阻塞呼叫。
GO TO FULL VERSION