你的 web 服務收到一個請求,需要:
- 查資料庫(50ms)
- 呼叫第三方 API(200ms)
- 寫快取(10ms)
如果這三件事依序做,一個請求要 260ms。如果 1 和 2 能同時進行,最長路徑是 200ms,快 30%。
問題是:「同時進行」這件事,在你的語言裡要怎麼表達?
OS Thread
作業系統的並行單元。每個 thread 有自己的 stack,OS 的 scheduler 決定什麼時候讓哪個 thread 跑在哪個 CPU 核心上。
Thread t = new Thread(() -> {
// 在獨立 thread 裡跑
System.out.println("thread running");
});
t.start();OS thread 的優勢是真正的平行(parallelism)——8 核機器可以同時在 8 個核心上跑 8 個 thread,CPU 密集任務(加密、壓縮、大量計算)能真正用到多核。
代價也很明顯:每個 OS thread 的 stack 預設 1-8 MB,一千個 thread 就要幾 GB;context switch(OS 在 thread 之間切換)有成本,幾 μs 到幾十 μs;共享記憶體需要鎖,鎖寫錯就是 deadlock 或 race condition。
Goroutine(Go 的 M:N scheduling)
Go runtime 管理的輕量執行單元,映射到 OS thread 上,但比 OS thread 便宜得多。
go func() {
// 啟動一個 goroutine,成本約 2KB stack
fmt.Println("goroutine running")
}()- 初始 stack 2KB(vs OS thread 的 1-8 MB),動態成長
- Go runtime 有自己的 scheduler(G-P-M 模型),N 個 goroutine 映射到 M 個 OS thread
- 當 goroutine 等 I/O,scheduler 自動把 OS thread 讓給其他 goroutine,不阻塞
一百萬個 goroutine 是可行的(2KB × 1M = 2GB)。一百萬個 OS thread 直接讓 OS 崩潰。
Channel:goroutine 之間通訊的機制,不用共享記憶體:
ch := make(chan int)
go func() {
ch <- 42 // 傳送
}()
result := <-ch // 接收Coroutine / async/await
協作式並行——不是 OS 決定什麼時候切換,而是程式主動說「我現在可以讓出控制」。
async def fetch_user(user_id):
data = await db.query(...) # 讓出控制,等 DB 回來
return dataawait 的意思是:「我在等一個 I/O 操作,event loop 可以去處理其他事情,等 I/O 完成再繼續我」。
和 goroutine 的關鍵差異:
async/await 是單 thread的(預設)。一個 Python asyncio 程式只有一個執行線,所有 coroutine 在這個線上協作輪流跑。你不能在 async def 裡做 CPU 密集計算,因為那會卡死整個 event loop,其他 coroutine 都動不了。
goroutine 是多 thread的——Go runtime 會把 goroutine 分散到多個 OS thread 上跑,CPU 密集任務不會卡死其他 goroutine。
為什麼 Python 的 asyncio 和 threading 不能隨便混
Python 有 GIL(Global Interpreter Lock)——同一時間只有一個 OS thread 能執行 Python bytecode,即使你開了多個 thread。
Thread 1: 執行 Python code
Thread 2: 等待 GIL,什麼都不做
所以 Python 的 threading 對 CPU 密集任務沒有幫助(GIL 讓你的多 thread 實際上是單 thread 輪流)。但 I/O 等待時 GIL 會釋放,所以 I/O 密集任務用 threading 是有效的。
asyncio 是單 thread,沒有 GIL 的問題,但只能用 await 協作,阻塞的程式碼(比如同步的資料庫驅動)會卡死整個 event loop。
multiprocessing 是多行程,真正的平行,但行程間通訊成本高。
三個工具解不同問題:
| 場景 | 解法 |
|---|---|
| I/O 密集、高並行 | asyncio 或 threading |
| CPU 密集 | multiprocessing |
| 混合(I/O + 少量 CPU) | asyncio + run_in_executor |
Actor Model(Erlang/Elixir)
每個 actor 是獨立的行程(Erlang 的輕量行程,不是 OS 行程),actor 之間只透過訊息傳遞溝通,沒有共享記憶體。
pid = spawn(fn ->
receive do
{:greet, name} -> IO.puts("Hello, #{name}")
end
end)
send(pid, {:greet, "World"})沒有共享記憶體 → 不需要鎖 → 沒有 deadlock 和 race condition。
Actor 的隔離性也帶來了 Fault isolation:一個 actor 崩了,不影響其他 actor。Erlang 的 Supervisor tree 可以自動重啟崩掉的 actor。WhatsApp 的後端每台伺服器撐兩百萬並行連線,靠的就是這個。
代價:訊息傳遞比共享記憶體貴;需要設計訊息協議;Erlang/Elixir 的生態系相對小。
選擇 primitive 的決策框架
這不是「哪個更好」,是「在什麼場景下哪個合適」:
| Primitive | 最適合的場景 | 注意的限制 |
|---|---|---|
| OS Thread | CPU 密集,需要真正平行 | 記憶體重,需要鎖 |
| Goroutine | I/O 密集 + CPU 密集混合 | 需要設計並行安全 |
| async/await | I/O 密集,單語言服務 | CPU 密集會卡死 event loop |
| Actor | 高隔離性,分散式,fault-tolerant | 訊息 overhead,生態小 |