1. 介紹
在程式設計中,新手(甚至有經驗的開發者)常常把兩個看起來相似但實際上不同的概念弄混:多執行緒 和 非同步。面試時常會問到這個問題,想看應徵者是不是真的理解差別,因為這會直接影響你如何寫出快速且回應靈敏的程式碼。
我們來釐清到底差在哪裡。
多執行緒:很多手一起工作
多執行緒 是指用多個執行緒來組織程式的工作。執行緒是一條執行線程,是處理器執行程式指令的獨立「通道」。一個程序(例如我們的 .NET 應用)可以啟動多個執行緒,讓不同任務同時進行。
生活中的例子: 你是專案經理,分派不同任務給不同同事,這些任務同時進行。有人寫報告、有人打電話給客戶、有人做簡報。
多執行緒的關鍵觀念: 任務是真正並行發生的(或者在單核心上透過快速切換上下文達到類似並行的效果)。
非同步:不空等、去做別的事
非同步 是把程式碼組織成在等待長時間操作(例如網路回應或檔案讀取)時能做別的事。非同步程式碼不一定會用到多個執行緒!它只是避免在等待某些操作時阻塞執行緒。
生活中的例子: 與其站在咖啡機前盯著咖啡滴,你把任務交給咖啡機,自己去回 e-mail、看新聞,等咖啡好了再去拿。
非同步的關鍵觀念: 等「長時間」任務時不要閒著,要去做有用的事情。
2. 差別在哪裡?
非同步與多執行緒常常一起使用,這也讓人更混亂。但事實上,這些技術回答的是不同的問題:
- 多執行緒 用來真實地平行處理工作,利用可用的處理器/核心。
- 非同步 用來智慧地組織等待資源(網路 I/O、磁碟 I/O 等),不去阻塞執行緒。
兩者可以結合使用,但它們不必然互相關聯。
簡短視覺化
| 多執行緒 (Threads) | 非同步 (Async) | |
|---|---|---|
| 何時使用? | 當任務佔用大量 CPU(CPU-bound:計算、渲染、處理陣列) | 當任務在等待事件(I/O-bound:網路、磁碟、資料庫) |
| 程式碼在做什麼? | 讓處理器跑滿,啟動多個執行緒,真正並行 | 在等待資料時釋放執行緒 — 執行緒可以去做其它事 |
| 管理什麼? | 同時執行的執行緒數量 | 誰現在忙/誰空閒,操作完成時要做什麼 |
| 典型範例 | 影片壓縮、影像渲染 | 檔案下載、向伺服器發送 HTTP 請求 |
範例 1:多執行緒 — 快速計算
假設我們有個重度任務:計算大數字的總和。
void ComputeSum(long start, long end)
{
long sum = 0;
for (long i = start; i <= end; i++)
{
sum += i;
}
Console.WriteLine($"總和從 {start} 到 {end} = {sum}");
}
// 同時啟動三個任務 — 每個計算自己的部分
Thread t1 = new Thread(() => ComputeSum(1, 1000_000_000));
Thread t2 = new Thread(() => ComputeSum(1000_000_001, 2000_000_000));
Thread t3 = new Thread(() => ComputeSum(2000_000_001, 3000_000_000));
t1.Start();
t2.Start();
t3.Start();
// 等待所有執行緒完成
t1.Join();
t2.Join();
t3.Join();
為什麼要用執行緒?
因為這是 CPU 密集的工作。如果你有多核心的機器,工作會被加速。
範例 2:非同步 — 等伺服器回應
網路很慢,在等待伺服器回應時,執行緒可以是「空閒」的。
// 非同步下載網頁,執行緒不會被阻塞
async Task DownloadPageAsync()
{
using HttpClient client = new HttpClient();
string html = await client.GetStringAsync("https://dotnet.microsoft.com/");
Console.WriteLine(html.Length);
}
為什麼這裡要用非同步?
我們下達「開始下載」的指令,同時不去阻塞執行緒,等待資料到達的通知。
3. 非同步而沒有多執行緒:神話還是真實?
問題: 任何非同步程式碼都一定會啟動新執行緒嗎?
答案: 不!很多時候非同步根本不需要額外的執行緒。
例如當你呼叫 await file.ReadAsync(...) 時,.NET 會在作業系統層面啟動非同步操作,呼叫該方法的執行緒會立即變成「空閒」並回到執行緒池。操作完成時,執行緒池中的任一空閒執行緒會繼續執行你的工作。
- 如果你改用同步版(file.Read(...))— 執行緒會傻等操作完成,什麼也不做。
- 非同步程式碼的意思是:「處理器,等會兒我們在等的時候請去做別的事情!」
重要示意:
// 執行緒不會被阻塞,只是在等待操作完成
await Task.Delay(1000); // 只等一秒 — 不會佔用 CPU!
沒有非同步的多執行緒
有些情況下只有用執行緒平行化工作才有意義:重運算、大量資料處理等。這種情況下,用非同步並不能加速實際的計算,因為 CPU 本來就會被吃滿。
經典:處理大型檔案
// 這段程式碼真的會吃掉 CPU — 非同步無法幫忙
void CalculateHash(string file)
{
byte[] data = File.ReadAllBytes(file); // 同步!
// 計算雜湊...
}
想要加速就啟動多個執行緒,每個處理自己的檔案。
4. 在你的應用程式中長什麼樣子?
非同步操作 (await)
在我們的教學應用中,可以加上非同步載入資料。例如請求匯率或天氣資料 — 最好用非同步。
async Task GetWeatherAsync(string city)
{
using HttpClient client = new HttpClient();
string json = await client.GetStringAsync($"https://api.weather.com/{city}");
// 收到回應後繼續處理
Console.WriteLine($"天氣在 {city}: {json}");
}
底層發生了什麼?
呼叫 await 會把你的方法切成兩部分:
- 發起非同步操作 — 作業系統執行緒「被釋放」,可以去做其他事。
- 當資料到達時 — 方法在執行緒池中的某個空閒執行緒上繼續執行。
複雜計算用多執行緒
在我們的計算機範例(假設要處理大陣列)— 這時啟動單獨執行緒來做計算是有意義的。
// 把大任務分成小塊,每個執行緒計算自己的範圍
List<Thread> threads = new List<Thread>();
for (int i = 0; i < 4; i++)
{
int rangeStart = i * 1000000;
int rangeEnd = (i + 1) * 1000000 - 1;
Thread t = new Thread(() => ComputeSum(rangeStart, rangeEnd));
threads.Add(t);
t.Start();
}
// 等待所有執行緒完成
foreach (Thread t in threads) t.Join();
5. 常見錯誤與非同步工作時的注意事項
錯誤 #1:把 async 當作「加速」手段。
很常見的誤解是認為 async 會讓程式跑得更快。事實不是這樣。非同步是為了回應性,而不是為了原地加速。
如果任務是 CPU-bound(處理器負載)— 非同步不會讓它更快。
如果任務是 I/O-bound(網路、磁碟)— 非同步很有用,因為執行緒不會閒置,可以去做其他工作。
錯誤 #2:透過 Wait() 或 Result 阻塞執行緒。
在非同步程式碼中千萬不要呼叫 Wait() 或存取任務的 Result。這幾乎總是會造成執行緒被阻塞和 deadlock(死結)。
// 不好!會阻塞執行緒並造成問題
var result = GetDataAsync().Result;
async Task<string> GetDataAsync() { /* ... */ return "data"; }
正確做法是使用 await,不要阻塞執行緒。
錯誤 #3:非同步與 UI。
在圖形界面應用(WPF、WinForms)中,主要的問題是不要凍結 UI 執行緒。如果在主執行緒做長時間或阻塞的操作,整個應用會「卡住」。非同步能解決這個問題:把重工作放到背景去做,介面保持回應。
錯誤 #4:沒有統一的非同步命名慣例。
如果不在非同步方法名稱加上後綴 Async,很容易搞不清哪個方法是同步的、哪個是非同步的。這會導致意外阻塞和呼叫錯誤。請務必在非同步方法名稱末尾加上 Async。
GO TO FULL VERSION