如果你要解釋「並行」給一個後端新人聽,你可能會這樣說:
「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 的 asyncio 和 threading 是不同的解法。
這是把兩層知識混在一起說的問題。
兩個層次,兩種更新節奏
共通概念層:並行是什麼?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.Map 和 maps.Clone 怎麼用?Python 3.12 的 asyncio 有哪些新 API?Node.js 的 worker_threads 和 cluster 的差異?
這些細節每隔一兩個版本就可能改。你花時間深入 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 便宜」。
讀者路徑是這樣的:
- 在 B01 建立心智模型——「並行有這幾種解法,各自的取捨是這樣」
- 在
language/go/看 Go 怎麼落地——「Go 選了這種解法,具體 API 是這些,坑在這裡」 - 在
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 系統共有的結構性約束。這時候文件不再是一堆要背的規則,是一個你能理解的設計選擇。