目錄
- 使用者執行緒 vs 核心執行緒
- 三種執行緒模型
- 1:1 模型(核心級)
- N:1 模型(使用者級)
- M:N 模型(混合)
- 協程(Coroutine)
- 實例:Goroutine / 虛擬執行緒 / async
- 怎麼選
- 常見問題
- 總結
使用者執行緒 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