執行緒區域儲存(thread-local storage)
/ TLS, said by its letters /
有時你想要一個範圍上是全域、但對每個執行緒私有的變數——例如 errno,兩個執行緒同時做系統呼叫時絕不能蓋掉彼此的錯誤碼。若 errno 是單一共享全域,執行緒就會競爭;若你手動到處傳遞它,每個呼叫的簽章都會膨脹。執行緒區域儲存是這樣的機制:它自動給每個執行緒一份指定變數「自己的」獨立副本,而你寫的程式碼只是照常使用那個變數的名字。
在 C 中你用關鍵字 _Thread_local(C11)或較舊的編譯器關鍵字 __thread 標記這種變數,例如 '_Thread_local int errno_value;'。實作保留一小塊每執行緒記憶體,叫執行緒控制區塊區域,而每個執行緒得到自己的區塊。每個執行緒區域變數在那個區塊內被指定一個固定「偏移量」,而一個特殊的 CPU 暫存器(在 x86-64 上,fs 暫存器指向執行緒區塊)讓程式碼能一步找到目前執行緒的區塊。於是讀取一個執行緒區域變數就變成「從 fs 暫存器取得每執行緒基底、加上這個變數的固定偏移量、讀取」。動態連結器與 C 執行期合作為每個執行緒設立其區塊,包括初始化有非零初始值的變數,並安排偏移量好讓主可執行檔與每個共享函式庫得到不重疊的空間(有幾種 TLS 存取模型——local-exec、initial-exec,以及 dlopen 載入的函式庫所用、較通用的 dynamic 模型——以速度換靈活性)。
它重要,是因為 TLS 是執行緒安全函式庫在不用鎖或不顯式傳遞下保有每執行緒狀態的方法:errno、地區設定緩衝區、配置器快取、亂數狀態都常住在 TLS 裡。誠實的告誡:執行緒區域變數不是共享的,所以你不能用它在執行緒間通訊;它的位址只在擁有它的執行緒內有效,所以把某執行緒區域變數的 &x 傳給另一個執行緒是個臭蟲;而動態載入的函式庫中對 TLS 的最最開始第一次存取可能稍慢,因為它可能需要額外一次執行期呼叫來定位區塊。
_Thread_local int counter = 0; // 每個執行緒得到自己的 'counter' // 在 x86-64 上,讀取它大致是: // mov eax, fs:[counter@tpoff] ; fs = 此執行緒的 TLS 基底 // 兩個執行緒遞增 'counter' 從不互相干擾。
每個執行緒有自己的 TLS 區塊,透過 fs 暫存器找到;執行緒區域變數在那個每執行緒區塊內的固定偏移量處被讀取。
執行緒區域變數是每執行緒的,所以不能用來在執行緒間共享資料,而且它的位址只在擁有它的執行緒內有意義——把某執行緒區域變數的 &x 傳給另一個執行緒是個臭蟲。用它存每執行緒狀態,不是用來通訊。