CodeGym /課程 /C# SELF /阻塞呼叫的問題

阻塞呼叫的問題

C# SELF
等級 59 , 課堂 0
開放

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# 裡這兩個關鍵字很重要:asyncawait

它們可以做到:

  • 不阻塞執行緒(特別是寶貴的 UI 或伺服器執行緒)。
  • 避免無謂地佔用資源。
  • 改善 GUI 的回應性。
  • 不會在慢操作進行時「佔著」執行緒。

注意: 我們不建議一開始就盲目使用非同步,因為它需要理解執行緒與阻塞。現在你已經理解了阻塞帶來的痛苦,非同步的方式會讓你和使用者都爽很多。

表格:什麼會阻塞,什麼不會?

方法/呼叫 會阻塞執行緒嗎 適合 UI/Server 嗎
Console.ReadLine()
不適合
Thread.Sleep(1000)
不適合
File.ReadAllText()
不適合
Task.Delay(1000).Wait()
不適合
HttpClient.GetStringAsync().Result
不適合
await File.ReadAllTextAsync()
不會 適合
await Task.Delay(1000)
不會 適合
await HttpClient.GetStringAsync()
不會 適合

備註: 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() 會把即便寫得很漂亮的非同步程式碼變回普通的阻塞呼叫。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION