執行緒模型與協程完全指南

使用者執行緒 vs 核心執行緒、1:1 / N:1 / M:N 模型,以及協程、纖程、goroutine、虛擬執行緒的關係


目錄


使用者執行緒 vs 核心執行緒

「執行緒」其實有兩種,理解它們的對應關係是全篇關鍵:

類型 由誰管理 OS 是否看得到
核心執行緒(Kernel Thread) 作業系統核心 是,由 OS 直接排程
使用者執行緒(User Thread) 使用者空間的執行庫 / runtime 否,OS 看不到

關鍵事實:只有核心執行緒會被 OS 排上 CPU。 使用者執行緒最終必須「映射」到某條核心執行緒上才能真正執行。這個映射關係,就是執行緒模型。

使用者空間:  User Thread(協程 / goroutine …)← runtime 自行排程
                      │ 必須映射到 ↓
核心空間:    Kernel Thread ← OS 排程,真正上 CPU

三種執行緒模型

依「使用者執行緒 : 核心執行緒」的對應比例分三種:

模型 對應 代表
1:1 一條使用者執行緒對一條核心執行緒 Linux pthreads、Java 平台執行緒、Windows
N:1 多條使用者執行緒對一條核心執行緒 早期 green threads
M:N M 條使用者執行緒對 N 條核心執行緒 Go goroutine、Java 虛擬執行緒

1:1 模型(核心級)

每建立一條(語言層)執行緒,就對應一條核心執行緒,由 OS 直接排程。

User:    T1    T2    T3
          │     │     │   (一對一)
Kernel:  K1    K2    K3
          ↓     ↓     ↓
         OS 排程,可跨多核並行
  • ✅ 真正利用多核(並行);一條阻塞不影響其他
  • ❌ 每條執行緒都是 OS 資源,建立成本高、數量受限(通常上限數千~數萬)
  • 代表:Linux pthread、Java 傳統平台執行緒、Windows 執行緒

N:1 模型(使用者級)

多條使用者執行緒全部映射到同一條核心執行緒,由使用者空間的 runtime 自行排程。

User:    T1  T2  T3  T4   ← runtime 在使用者空間切換(快)
              │(全部對一)
Kernel:       K1
         OS 只看到一條
  • ✅ 建立 / 切換極輕量(不進核心)、可開大量
  • 無法利用多核(只有一條核心執行緒);一條阻塞式系統呼叫會卡住全部(OS 不知道還有別的使用者執行緒可跑)
  • 代表:早期的 green threads

M:N 模型(混合)

M 條使用者執行緒,動態調度到 N 條核心執行緒上(N 通常接近核心數)。兼具兩者優點,是現代高並發 runtime 的主流。

User:   g1 g2 g3 g4 g5 g6 …   ← 大量輕量使用者執行緒
          ╲  │  ╱   ╲ │ ╱
Kernel:    K1          K2      ← 少量核心執行緒(≈核心數)
            ↓           ↓
          Core0       Core1    ← 真正並行
  • ✅ 可開海量使用者執行緒、切換輕量,又能利用多核並行
  • ✅ runtime 偵測到阻塞時,把其他使用者執行緒移到別的核心執行緒繼續跑
  • ❌ runtime 排程器實作複雜
  • 代表:Go 的 goroutine(GMP 模型)Java 21 虛擬執行緒(Project Loom)

協程(Coroutine)

協程是一種可以暫停(suspend)與恢復(resume) 的函式:執行到一半主動讓出,稍後從原處繼續。它是協作式的使用者層並發單位。

一般函式:呼叫 → 從頭跑到 return → 結束
協程:    呼叫 → 跑到 yield/await 暫停 → 之後 resume 從原處續跑
  • 由 runtime / 語言在使用者空間排程,不是 OS 排程單位
  • 切換在明確的 await / yield 點發生(協作式),開銷遠小於核心執行緒切換
  • 特別適合大量 I/O 等待的場景(網路服務、爬蟲)

相關名詞釐清

名詞 說明
Coroutine(協程) 可暫停 / 恢復的函式,協作式
Fiber(纖程) 與協程概念相近的輕量使用者執行緒,需手動排程
Green Thread 早期由 VM / runtime 模擬的使用者執行緒(多為 N:1)
async/await 語言語法糖,背後常以協程 / 事件迴圈實現非阻塞並發

實例:Goroutine / 虛擬執行緒 / async

Go:Goroutine(M:N)

  • go f() 啟動一條 goroutine,初始 stack 僅約 2KB,可同時開數十萬條
  • GMP 模型:G(goroutine)、M(核心執行緒 machine)、P(處理器邏輯,持有可執行 G 佇列)
  • runtime 把 G 排到 M 上,遇阻塞自動搬移 → 多核並行 + 海量並發
  • 細節見 Go Goroutines 完全指南

Java:平台執行緒 vs 虛擬執行緒

  • 平台執行緒:傳統 1:1,對應 OS 執行緒,數量受限
  • 虛擬執行緒(Java 21, Project Loom):M:N,海量輕量執行緒掛在少數載體執行緒(carrier thread)上,適合高並發 I/O
  • 細節見 Java 多執行緒完全指南

Python / JS:async/await(事件迴圈)

  • 單執行緒 + 事件迴圈,靠協程在 await 點切換,達成高並發 I/O(非並行)
  • Python 另受 GIL 限制,CPU 密集仍需多行程

怎麼選

需求 適合
CPU 密集、要真並行 核心執行緒(1:1)/ 多核 + 適量執行緒
海量 I/O 並發連線 M:N(goroutine / 虛擬執行緒)或協程 / async
簡單、少量並發 直接用語言預設執行緒
需要強隔離 / 容錯 多行程(見 行程與執行緒

重點認知:協程 / goroutine 的「輕量」來自它們不是 OS 排程單位,切換不進核心。但要真正並行,底層仍得靠多條核心執行緒(M:N 的 N)。


常見問題

問題 1:協程是執行緒嗎?

不是 OS 意義的執行緒。協程是使用者空間可暫停 / 恢復的並發單位,由 runtime 協作式排程,OS 看不到它,也不直接排程它。

問題 2:goroutine 和執行緒差在哪?

goroutine 是使用者層輕量執行緒(M:N),初始 stack 極小、可開海量、切換不進核心;OS 執行緒(1:1)較重、數量受限。goroutine 最終仍跑在核心執行緒上。

問題 3:為什麼 N:1 一條阻塞會卡住全部?

因為 OS 只看到那一條核心執行緒。當它執行阻塞式系統呼叫被 OS 掛起,整條核心執行緒停擺,上面的所有使用者執行緒都動不了——OS 不知道還有別的可跑。M:N 透過 runtime 偵測並搬移來解決。

問題 4:協程能利用多核嗎?

單純協程 / async(單執行緒事件迴圈)不行,只有並發沒有並行。要利用多核需搭配多條核心執行緒(M:N,如 goroutine),或多行程。

問題 5:虛擬執行緒會取代平台執行緒嗎?

虛擬執行緒適合高並發 I/O,大幅降低每條執行緒的成本;但 CPU 密集型工作仍受實際核心數限制,兩者是互補而非全面取代。


總結

核心要點

  • 執行緒分使用者執行緒(runtime 管,OS 看不到)與核心執行緒(OS 排程、真正上 CPU)
  • 三模型:1:1(核心級,真並行但重)、N:1(使用者級,輕但無法多核、一阻塞全卡)、M:N(混合,海量 + 多核)
  • 協程是協作式、可暫停 / 恢復的使用者層並發單位,不是 OS 排程單位
  • goroutine(GMP)Java 虛擬執行緒(Loom) 是 M:N 的代表;async/await 多為單執行緒事件迴圈
  • 輕量來自「不進核心切換」,但真並行仍需底層多條核心執行緒

快速參考

模型 多核並行 海量輕量 一阻塞卡全部 代表
1:1 pthread / Java 平台執行緒
N:1 ⚠️ 會 早期 green threads
M:N goroutine / 虛擬執行緒

建立日期:2026-06-18

🔗相關文章