核心層級執行緒(kernel-level threads)
現在想像和那位神祕經理相反的情況:每位員工都在總公司登記有案,總公司分配座位、決定誰何時上工。總公司能個別看見每位員工,也能在各辦公室之間調動他們。核心層級執行緒就像這樣:作業系統本身知道、並親自排程的執行緒。核心為每條執行緒維護記錄,並直接把執行緒安排到 CPU 核心上。
具體來說,採用核心層級執行緒時,作業系統為每條執行緒保有一筆記錄(它自己的執行緒控制結構),核心的排程器在決定每個 CPU 核心接下來該跑什麼時,是從「執行緒」之間挑選,而不只是從「行程」之間挑選。因此建立執行緒、在執行緒之間切換、以及讓它們同步,都要透過系統呼叫進入核心,這使得每一項操作都比純粹在行程內切換的使用者層級執行緒來得重。今天幾乎每個通用作業系統(Linux、Windows、macOS)都提供核心層級執行緒,作為對使用者開放的執行緒 API 所建立的基礎。
它的回報正是使用者層級執行緒做不到的。因為核心會獨立排程每條執行緒,一個行程的兩條執行緒可以在同一瞬間跑在兩個不同的核心上,得到真正的平行。而且如果一條執行緒做了會阻塞的系統呼叫,核心可以直接把同一行程的另一條執行緒排上 CPU,於是整個行程不會凍結。代價就是那個關鍵:每一次執行緒操作都要進入核心,所以核心執行緒在建立與切換上都比使用者執行緒重,這正是為什麼為每個微小任務各建一條執行緒很浪費,也正是執行緒池模式存在的原因。
在 Linux 上,你用 pthreads 建立的執行緒會對應到核心執行緒,所以一個做繁重運算的四執行緒程式可以同時佔滿筆電的四個核心。而當其中一條執行緒因網路讀取而阻塞時,核心會讓其餘三條繼續跑,而不是把整個程式停住。
核心會排程每一條執行緒,所以它們能得到真正的平行,代價是每次操作的成本較高。
核心執行緒換來真正的平行,而且某條執行緒阻塞時其他仍能存活,但每一次建立、切換與同步都要進入核心,因此比使用者層級的切換更貴。這種成本正是執行緒池勝過「每任務一執行緒」的原因。