在一般應用程式中,thread 要在哪一顆 CPU 上執行,不會是開發者需要操心的問題。作業系統的 scheduler 會根據 CPU 負載、Thread priority、CPU topology、NUMA locality 以及其他排程因素,自動決定 runnable thread 應該被派到哪個 logical CPU。
這種設計對大多數 workload 都非常合理。但當系統進入另一種情境 —— 例如低延遲交易、封包處理、遊戲引擎、音訊處理、storage I/O、telemetry pipeline、訊息 broker 或高頻資料處理,各種隱憂就開始浮現。
這就是 thread pinning 的價值所在 —— 用 CPU affinity 降低 scheduler jitter,確保 critical thread 在需要執行的那一刻,能以高度確定性穩定取得 CPU。若再進一步結合 CPU isolation,更能排除其他程序與中斷的干擾,達到極致的低延遲。
1. 為什麼 Scheduler 會造成 Jitter?
現代作業系統的 scheduler 非常複雜。它的主要目標通常是讓整體系統具有良好的 throughput、fairness 與 responsiveness,而不是讓某一個 thread 永遠獲得最低可能的 scheduling latency。
while (running) {
receive();
process();
send();
}
上述例子是一個 latency-sensitive loop。我們希望這個 loop 能夠持續以非常穩定的時間間隔執行,然而實際情況可能變成:
CPU
ㅤ├── critical thread
ㅤ├── kernel task
ㅤ├── interrupt handler
ㅤ├── another user thread
ㅤ├── kworker
ㅤ└── background process
當 critical thread 正在 CPU 上執行時,可能突然遇到:
因此,即使程式本身的 execution time 非常穩定,實際觀察到的 latency 仍可能出現尖峰。高效能 concurrency 的主要難題之一,是如何降低這些 tail latency。
2. Thread Pinning 到底在做什麼?
Thread pinning 的核心概念很簡單:限制某個 thread 可以在哪些 CPU 上執行。最常見的方式就是 CPU affinity。
例如一台機器有 8 個 CPU。一般情況下,一個 thread 可能可以被 scheduler 放到 CPU 0~7。如果我們把這個 critical thread pin 到 CPU 6,那麼 scheduler 就不會把這個 critical thread 任意 migrate 到其他 CPU。
在 Linux 上,可以透過 CPU affinity API 完成:
cpu_set_t cpuset;
CPU_ZERO(&cpuset);
CPU_SET(6, &cpuset);
pthread_setaffinity_np(
pthread_self(),
sizeof(cpu_set_t),
&cpuset
);
這並不表示 CPU 6 比 CPU 0 快,而是降低了 scheduler 為了平衡整體系統而移動這個 thread 的自由度。這種控制對某些 workload 非常重要。
3. CPU Migration 為什麼值得注意?
Thread migration 看起來只是 CPU 2 → CPU 6 這麼輕易,但實際上會牽涉更多成本。最明顯的是 CPU cache locality。
假設 Thread 在 CPU 2 上執行一段時間:
CPU 2
ㅤ├── L1 cache
ㅤ├── L2 cache
ㅤ└── registers/state
它處理的資料可能已經大量存在於 CPU 2 的 cache hierarchy 中。如果 scheduler 將 Thread migrate 到 CPU 6,CPU 6 需要重新建立自己的 cache working set:
CPU 2ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤCPU 6
cacheㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ cache
ㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │
ㅤ└──── thread ───────►
當然,這不代表每次 migration 都會造成巨大成本。現代 CPU 的 cache coherence 與 scheduler 都已經非常成熟,但是在極低 latency workload 中,即使微小的 locality 變化也可能反映在 tail latency。
因此 Thread pinning 的第二個目的就是維持 execution locality。
4. Pinning 不等於「CPU 專用」
這是一個非常重要的觀念。很多人第一次接觸 affinity 時會認為:Thread A 綁定到 CPU 6 代表 CPU 6 專屬於 Thread A。其實並非如此,affinity 只表示限制該執行緒的調度範圍:
Thread A
allowed CPUs = {6}
Thread A 只能在 CPU 6 上執行,但 CPU 6 上仍然可能存在於其他 thread,或是 IRQ、softirq、kernel worker 和 other kernel activity。所以 pinning 與 CPU isolation 是兩個不同層次的概念。前者控制這個 thread 可以去哪個 CPU;後者則更進一步控制這顆 CPU 上還允許什麼工作發生。這也是為什麼真正的 high-performance setup 往往不只使用 thread pinning。
5. 從 CPU Affinity 走向 Isolated CPU
如果我們真的希望建立一個 latency-sensitive execution core,可以考慮把 CPU topology 設計成:
CPU 0ㅤsystem / housekeeping
CPU 1ㅤsystem / housekeeping
CPU 2ㅤsystem / housekeeping
CPU 3ㅤsystem / housekeeping
CPU 4ㅤisolated
CPU 5ㅤisolated
CPU 6ㅤisolated
CPU 7ㅤisolated
Application
ㅤ├── networking thread → CPU 4
ㅤ├── processing thread → CPU 6
ㅤ└── worker thread → CPU 7
系統的一般背景工作盡量集中在 housekeeping CPUs。這樣做並非是為了讓 CPU 4~7 完全沒有任何作業系統活動,實際上現代 OS 也很難做到絕對意義上的零干擾。真正的目標是把不可避免的 system activity 與 critical execution path 分離,因此可以把整體架構想成:
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ┌───────────┐
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ│ㅤHousekeeping CPUㅤㅤ│
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ│ㅤㅤOS / IRQ / miscㅤㅤㅤ│
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ└─────┬─────┘
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ⠀ㅤ│
ㅤ⠀Applicationㅤ────────┼───────
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ⠀ㅤ│
ㅤㅤㅤㅤㅤㅤㅤㅤㅤ┌──────┴──────┐
ㅤㅤㅤㅤㅤㅤCPU 4 isolatedㅤㅤㅤㅤㅤㅤㅤㅤㅤCPU 6 isolated
ㅤㅤㅤㅤㅤㅤㅤㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ│
ㅤㅤㅤㅤㅤㅤㅤRX loopㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤprocessing loop
ㅤㅤㅤㅤㅤㅤㅤㅤㅤ└────ㅤdataㅤ─────┘