如果你要解釋「並行」給一個後端新人聽,你可能會這樣說:

「Go 裡面用 go 關鍵字啟動 goroutine,然後用 chan 在 goroutine 之間傳資料。Node.js 的話,因為是 single-thread 的,你用 async/await 或 Promise 來處理非同步。Python 有 asyncio,但有 GIL 的限制,所以 CPU 密集的任務要用 multiprocessing…」

這樣說沒有錯,但你會發現自己說了很多語言特定的細節,但「並行是什麼」這個問題還沒回答。聽的人知道了 go 關鍵字,但不知道為什麼 goroutine 比 OS thread 便宜、為什麼 Node.js 選 single-thread、為什麼 Python 的 asynciothreading 是不同的解法。

這是把兩層知識混在一起說的問題。


兩個層次,兩種更新節奏

共通概念層:並行是什麼?Event loop 的機制是什麼?GC 怎麼標記和回收?Exception 和 Result type 各自的取捨是什麼?

這些問題的答案,十年前和十年後大致相同。Go 的 goroutine 用 M:N scheduling,這個原理在 Go 1.x 到現在沒有根本性的改變。GC 的 tri-color marking algorithm,在 Go 和 Java 的 GC 裡都適用,不因版本而變。

語言特定層:Go 1.21 的 sync.Mapmaps.Clone 怎麼用?Python 3.12 的 asyncio 有哪些新 API?Node.js 的 worker_threadscluster 的差異?

這些細節每隔一兩個版本就可能改。你花時間深入 Go 1.18 的某個 API,Go 1.21 可能已經有更好的替代品。

把這兩層放在一起寫,共通概念會淹沒在版本細節裡,語言特定的內容也會因為要解釋底層原理而失焦。


這個系列的分工

B01(這個系列)只講共通概念層:並行的模型是什麼、記憶體管理的三種哲學、型別系統的設計選擇。不在這裡說「Go 的 goroutine API 長什麼樣」。

backend/language/{name}/(語言特定系列,未來的 B05 延伸)只講語言特定層:Go 的 goroutine 實際怎麼用、channel 的坑在哪、context.Context 為什麼要到處傳。不在那裡重複解釋「為什麼 M:N scheduling 比 OS thread 便宜」。

讀者路徑是這樣的:

  1. 在 B01 建立心智模型——「並行有這幾種解法,各自的取捨是這樣」
  2. language/go/ 看 Go 怎麼落地——「Go 選了這種解法,具體 API 是這些,坑在這裡」
  3. backend/framework/generic/ 看框架怎麼在語言之上再抽象——「FastAPI 選 async,Django 選 sync,原因是這個」

每一層都有自己的視角,不重複,互相引用。


實際帶來的差異

把這兩層分開之後,有一件事會變清楚:

當你看到一個語言做了某個選擇,你會有地方去問「為什麼」。

為什麼 Go 選 goroutine 不選 OS thread?→ 去 B01-12(並行 primitives) 找 M:N scheduling 的原理。
Go 的 goroutine 實際用起來是什麼感覺?→ 去 language/go/ 找具體範例。

這兩個問題都有答案,但不在同一個地方。你在找「為什麼」的時候不會淹沒在 API 文件裡,在找「怎麼寫」的時候不會卡在理論解釋裡。

舉個更具體的例子:你在讀 Python 的 asyncio 文件,發現「coroutine 不能呼叫同步阻塞函式」這個限制,覺得很奇怪。去 B01-13(IO 模型) 找 event loop 的機制,你會知道這個限制不是 Python 的設計失誤,而是所有 event-driven 系統共有的結構性約束。這時候文件不再是一堆要背的規則,是一個你能理解的設計選擇。

下一篇:後端語言演進史