上下文切換(context switch)
當想跑的執行緒比 CPU 核心多時,作業系統用時間來分享每個核心:讓一條執行緒跑一小段,然後暫停它、讓另一條跑,如此反覆——快到它們看起來像一起前進。在一個核心上暫停一條執行緒、恢復另一條的這個動作,就是上下文切換(context switch)。想像一位西洋棋大師同時下好幾盤:她走到一盤、下一步、走去下一盤,如此繼續——每盤的局面都原封不動地留著,等她稍後再接手。
「上下文」是那條被暫停的執行緒稍後要從原處繼續所需的一切:CPU 暫存器的內容(包括標記下一條指令的程式計數器、以及堆疊指標)以及相關的記帳。要切換,作業系統先存下正在跑的執行緒的上下文(把那些暫存器複製到記憶體),再把下一條執行緒先前存下的上下文載回暫存器,於是 CPU 從那條執行緒的半途接手,彷彿它從未停過。核心裡的排程器決定接下來誰跑、跑多久。在同一行程內的兩條執行緒之間切換相對便宜,因為它們共享一個定址空間;在不同行程的執行緒之間切換還牽涉到更換記憶體映射,花費更多。
上下文切換對我們的危害為何重要:上下文切換幾乎能落在任何指令邊界,包括讀-改-寫或任何臨界區間的中途。這正是兩條執行緒的操作被交錯的機制——作業系統在 count++ 做到一半時暫停執行緒 A、去跑執行緒 B,造成那個遺失更新的錯誤。上下文切換也有代價:它們花時間,還可能把有用的資料從 CPU 快取裡擠出去,所以切換得太瘋狂的程式會把力氣花在搬動執行緒、而非做事。這也是為什麼執行緒不是免費的、為什麼更多執行緒不一定更快的一部分原因。
在跑三條執行緒的單核上,作業系統可能讓執行緒 1 跑 10 毫秒、存下它的暫存器、還原執行緒 2 的、讓它跑 10 毫秒,如此循環——快到對人類而言三條看起來同時在跑,儘管任何時刻核心上其實只有一條。
存下一條執行緒的暫存器、載入另一條的——核心以時間分享,一次一條執行緒。
因為切換可發生在任兩條機器指令之間,你不能假設連一行 C 都不被打斷地跑完——那個假設正是讀-改-寫錯誤的來源。上下文切換也耗費真實時間,所以它們並非免費。