M:N 相對於 1:1 執行緒模型(M:N vs 1:1 threading)
假設你有一百件差事、十輛車。1:1 方案:買一百輛車、一件差事一輛——簡單,但車很貴,你的車道也擠爆了。M:N 方案:留著你的十輛車,加上一個聰明的調度員,讓一百件差事輪流用這些車,於是每輛車一天下來服務許多件差事。在執行緒裡,「差事」是你的邏輯任務(協程、goroutine、纖程),「車」是核心真正排到核心上的作業系統執行緒。這個比例命名了誰對應到誰。
在 1:1 模型裡,每條使用者執行緒由一條作業系統(核心)執行緒撐著——這正是 std::thread、pthreads 和 JVM 的平台執行緒給你的。核心直接排程每一條;它們是真實、可被搶佔、能在不同核心上真正平行跑的,但每條都背著一整條作業系統執行緒的成本(約 1 MiB 的堆疊、核心簿記、以及一次由核心居中的上下文切換)。在 M:N 模型裡,語言執行時期把 M 條輕量的使用者層級執行緒多工到 N 條作業系統執行緒上(N 通常在核心數附近)。這 M 個任務——Go 的 goroutine、Erlang 的行程、Java 新的虛擬執行緒——很便宜(goroutine 起始約 2 KiB),而它們之間的切換是快速的使用者空間操作,不勞核心呼叫。執行時期的排程器(常是工作竊取)在任務的讓出點把它停泊起來、在同一條作業系統執行緒上跑另一個,於是寥寥幾條作業系統執行緒能服務數十萬個任務。這跟綠色執行緒與有堆疊協程背後的機制是同一套。
為何這區分重要:1:1 很簡單,核心替你處理阻塞與搶佔,但你負擔不起一百萬條。M:N 便宜地給你龐大的並行——一百萬個 goroutine 沒問題——這對於兜著大量「多半在等待」之連線的伺服器是理想的。M:N 的經典陷阱、也是個誠實的陷阱:當一個使用者任務做了阻塞的系統呼叫,它阻塞了底層的作業系統執行緒,天真地看,這會凍住多工到該執行緒上的所有其他任務;生產級的 M:N 執行時期靠偵測到阻塞、再啟動或交接出另一條作業系統執行緒來解決(Go 會自動做這件事)。所以 M:N 不是免費的魔法——它需要一個精巧的執行時期,而一個處理不當的阻塞呼叫仍會卡死一條真正的執行緒。
1:1:std::thread::spawn(f)——每個任務一條作業系統執行緒,各約 1 MiB 堆疊。M:N:Go 的 'go f()'——數百萬個約 2 KiB 的 goroutine,被工作竊取的執行時期排程器多工到 GOMAXPROCS 條作業系統執行緒上。
1:1 把每個任務對應到一條核心執行緒;M:N 把許多便宜的使用者任務多工到少數核心執行緒上。
M:N 並非絕對更好:它需要一個聰明的執行時期,以防一個阻塞的系統呼叫卡死作業系統執行緒(並拖住同處的任務)。1:1 較重但較簡單,且核心免費替你處理搶佔與阻塞。多數語言只選一種;有些(Java)如今兩種都提供。