執行緒區域儲存(thread-local storage)
/ TLS (the storage kind, not the network kind) /
有時你想要一個變數,讓每條執行緒都能用同一個名字去指它,但每條執行緒卻悄悄各自拿到自己的私有副本。執行緒區域儲存(thread-local storage)正好給你這個。想像一家旅館,每個房間都有個標著「迷你吧」的東西:每間房的標籤都一樣,但每位客人的迷你吧都是自己的——一位客人拿走的東西碰不到另一位的。執行緒區域變數運作方式相同:一個名字,但每條執行緒各有一個獨立的盒子。
具體地說,普通的全域變數是所有執行緒共享的一個盒子(因此是共享可變狀態,帶著它全部的危害)。執行緒區域變數的宣告方式,會讓執行期給每條執行緒它自己的一份實例;執行緒 A 的副本和執行緒 B 的副本住在不同位址,寫一個永遠不影響另一個。在 C11 裡你用 _Thread_local 關鍵字標記這樣的變數(C++ 用 thread_local);較舊的程式碼用 __thread 之類的編譯器擴充。每條執行緒的副本在執行緒啟動時建立,活到那條執行緒結束。所以你得到全域的便利——從任何函式都搆得到、不必到處傳遞——卻沒有共享,因為它其實並未被共享。
它為何有用:執行緒區域儲存是治療共享可變狀態最乾淨的解法之一,因為它整個拿掉了「共享」那條腿。經典的真實例子是 errno:許多函式庫函式透過 errno 回報錯誤,若它是一個共享全域,兩條執行緒的錯誤碼會互相覆蓋——所以在有執行緒的系統上 errno 是執行緒區域的,給每條執行緒自己的一份。同樣的技巧也適合每條執行緒的暫存緩衝區、最後再加總的計數器、或任何邏輯上「屬於這條執行緒自己」的狀態。代價是執行緒區域資料是真正分開的:它不能用來在執行緒之間溝通,而且每份副本要花一點記憶體。
_Thread_local int call_count = 0; 給每條執行緒自己的 call_count。每條執行緒都能自由地做 call_count++ 而不上鎖、不競爭,因為任兩條執行緒不共享同一個——「共享可變狀態」中的「共享」沒了。
一個名字,每條執行緒各有私有副本——移除了共享,所以不需要鎖。
執行緒區域並不代表「共享但安全」——它代表「根本不共享」。你不能透過執行緒區域變數在執行緒之間傳資料,而且每條執行緒的副本各自獨立。(這是儲存的 TLS;別把它跟網路的 TLS,即傳輸層安全性,搞混。)