執行緒的成本(the cost of threads)
寫程式時執行緒感覺輕飄飄——呼叫一次 pthread_create() 你就多了一名工人——但它們並非免費,把它們當免費會導致程式「加越多執行緒跑得越糟」。要記住兩大成本:建立(與銷毀)一條執行緒的成本,以及執行緒跑起來時彼此切換的成本。想像雇用臨時工:每位新人都要花時間上手、要一張桌子坐,而若工人遠多於桌子,他們會整天換位子而不是工作。
逐一來看這些成本。建立一條執行緒比建立一整個行程便宜——執行緒共享定址空間,要設定的東西較少——但它仍是真實的工作:系統要配置一塊全新的堆疊(常保留一 MiB 以上的定址空間)、設好記帳、向排程器登記這條執行緒。衍生上千條短命執行緒,那份設定成本就會主導全局。第二項成本是上下文切換:每次作業系統暫停一條執行緒、恢復另一條,都得存下並還原暫存器狀態,而切換往往把有用的資料從 CPU 快取裡沖掉,所以被恢復的執行緒會跑得很慢,直到它的工作資料重新載入。當你的執行緒遠多於核心時,核心會把越來越大比例的時間花在切換而非計算上。
這在實務上為何重要:更多執行緒不會自動變成更多速度。一旦超過「忙碌的執行緒大約等於核心數」這個點,再加通常會更慢,因為多出來的執行緒只增加切換與記憶體爭用的開銷,卻沒增加任何真正的平行(沒有更多核心去跑它們)。標準的專業對策是:重用一個固定的執行緒池,而非每個小任務都建一條(執行緒池),並把活躍執行緒數量配合核心數量。執行緒是個強大的工具,但跟任何資源一樣有價碼,優秀的系統程式設計師會刻意地花用它們。
一個對每個進來的請求都衍生一條新執行緒、處理完再銷毀的伺服器,在負載下可能花在建立與拆除執行緒上的時間比服務請求還多。改用一個重用的池(比方說 16 條工人執行緒)從佇列拉取請求,就避免了這種反覆折騰。
執行緒在建立與切換上都要花時間;超過核心數後,更多執行緒通常代表更慢。
執行緒多於核心並不增加平行——只增加開銷。對 CPU 密集的工作,正確的執行緒數通常接近核心數;對大多在等待(磁碟或網路)的工作,多一些執行緒可能有幫助,因為它們重疊的是等待、而非計算。