線程局部儲存

線程局部儲存(英語:Thread-local storage,縮寫:TLS)是一種儲存持續期(storage duration),對象的儲存是線上程開始時分配,線程結束時回收,每個線程有該對象自己的實例。這種對象的連結性(linkage)可以是靜態的也可是外部的。

TLS的一個例子是用全域變數errno英語errno表示錯誤號。這可能在多線程並行時產生同步錯誤。線程局部儲存的errno是個解決辦法。

Windows的實現

每個行程都有一組標誌,共TLS_MINIMUM_AVAILABLE(==64)個。每個標誌可以被設為FREE或INUSE,表示該TLS元素是否正在使用。注意這組標誌屬行程所有。當系統建立一個線程的時候,會為該線程分配與線程關聯的、屬於線程自己的PVOID型陣列(共有TLS_MINIMUM_AVAILBALE個元素),陣列中的每個PVOID可以儲存任意值。

Windows API函數TlsAlloc用於取得行程中一個未用的TLS slot index。然後將該標誌從FREE改為INUSE,並返回該標誌在位陣列中的索引,通常將該索引儲存在一個全域變數中,因為這個值會在整個行程範圍內(而不是線程範圍內)使用。

呼叫TlsSetValue(dwTlsIndex,pvTlsValue)將一個PVOID值放到線程的陣列中dwTlsIndex指定的具體位置。

函數TlsGetValueTlsSetValue用於通過TLS slot index讀寫一個線程局部儲存變數所指向的主記憶體塊。函數TlsFree用於釋放TLS slot index

Win32線程資訊塊英語Win32 Thread Information Block的FS:[0x2C]地址處,存放的是線程局部儲存表的地址。[1]每個線程用它自己的線程局部儲存表的拷貝。TlsAlloc返回表中一個未使用的索引。因此每個線程可以用TlsSetValue(index)設置線程局部儲存值,用TlsGetValue(index)取得線程局部儲存值。

Windows可執行程式也可以定義一個(section),對映到行程每個線程的不同的主記憶體分頁。這種節只定義在主程式里,動態連結庫(DLL)不應該包含這種節因為不會被LoadLibrary函數在載入時初始化。

對於Windows系統來說,全域變數或靜態變數會被放到".data"或".bss"段中,但當使用__declspec(thread)定義一個線程私有變數的時候,編譯器會把這些變數放到PE檔案的".tls"段中。當系統啟動一個新的線程時,它會從行程的堆中分配一塊足夠大小的空間,然後把".tls"段中的內容複製到這塊空間中,於是每個線程都有自己獨立的一個".tls"副本。所以對於用__declspec(thread)定義的同一個變數,它們在不同線程中的地址都是不一樣的。對於一個TLS變數來說,它有可能是一個C++的全域對象,那麼每個線程在啟動時不僅僅是複製".tls"的內容那麼簡單,還需要把這些TLS對象初始化,必須逐個地呼叫它們的全域建構函式,而且當線程退出時,還要逐個地將它們解構,正如普通的全域對象在行程啟動和退出時都要構造、解構一樣。Windows PE檔案的結構中有個叫數據目錄的結構。它總共有16個元素,其中有一元素下標為IMAGE_DIRECT_ENTRY_TLS,這個元素中儲存的地址和長度就是TLS表(IMAGE_TLS_DIRECTORY結構)的地址和長度。TLS表中儲存了所有TLS變數的建構函式和解構函式的地址,Windows系統就是根據TLS表中的內容,在每次線程啟動或退出時對TLS變數進行構造和解構。TLS表本身往往位於PE檔案的".rdata"段中。

Pthreads的實現

Pthreads API定義了線程特定的數據。

函數pthread_key_createpthread_key_delete建立與刪除一個鍵,用於線程特定的數據。鍵的類型被稱為pthread_key_t。鍵可以被所有線程看到。在每個線程,鍵可以用pthread_setspecific函數關聯到線程特定的數據。數據可以隨後用pthread_getspecific函數取得。

特定於語言的實現

C and C++

C11的關鍵字_Thread_local用於定義線程局部變數。在標頭檔<threads.h>定義了thread_local為上述關鍵詞的同義。例如:

#include <threads.h>
thread_local int foo = 0;

C++11引入了thread_local[2]關鍵字用於下述情形:

  • 命名空間(全域)變數
  • 檔案靜態變數
  • 函數靜態變數
  • 靜態成員變數

此外,不同編譯器提供了各自的方法聲明線程局部變數:

Windows的版本早於Vista與Server 2008, __declspec(thread)對於DLL只用於DLL被可執行程式繫結靜態載入,在LoadLibrary()函數動態載入DLL將報告protection fault或data corruption。[9]

Java

Java語言中,線程局部變數使用ThreadLocal類對象表示。ThreadLocal保持了變數的類型T,可以通過get/set方法訪問。例如,ThreadLocal保持了Integer值:

private static final ThreadLocal<Integer> myThreadLocalInteger = new ThreadLocal<Integer>();

Oracle/OpenJDK使用作業系統線程以避免效能代價。[10]

.NET 語言: C# 與Visual Basic.Net

.NET Framework語言,靜態域可標記ThreadStatic attribute頁面存檔備份,存於互聯網檔案館):

class FooBar {
   [ThreadStatic] static int foo;
}

.NET 4.0,System.Threading.ThreadLocal<T>頁面存檔備份,存於互聯網檔案館)可用於分配與惰性裝入線程局部變數。

class FooBar {
   private static System.Threading.ThreadLocal<int> foo;
}

Also an API is available for dynamically allocating thread-local variables.

Python

Python語言從版本2.4開始,threading模組的local類可用於建立線程局部儲存:

import threading
mydata = threading.local()
mydata.x = 1

Ruby

Ruby語言能建立/訪問線程局部變數使用[]=/[]方法:

Thread.current[:user_id] = 1

參考文獻

  1. ^ Pietrek, Matt. Under the Hood. MSDN. May 2006 [6 April 2010]. (原始內容存檔於2016-03-03). 
  2. ^ Section 3.7.2 in C++11 standard
  3. ^ IBM XL C/C++: Thread-local storage頁面存檔備份,存於互聯網檔案館
  4. ^ GCC 3.3.1: Thread-Local Storage頁面存檔備份,存於互聯網檔案館
  5. ^ Clang 2.0: release notes頁面存檔備份,存於互聯網檔案館
  6. ^ Intel C++ Compiler 8.1 (linux) release notes: Thread-local Storage頁面存檔備份,存於互聯網檔案館
  7. ^ Visual Studio 2003: Thread extended storage-class modifier頁面存檔備份,存於互聯網檔案館
  8. ^ Intel C++ Compiler 10.0 (Windows平台): Thread-local storage頁面存檔備份,存於互聯網檔案館
  9. ^ "Rules and Limitations for TLS". [2018-09-13]. (原始內容存檔於2008-04-04). 
  10. ^ How is Java's ThreadLocal implemented under the hood?. Stack Overflow. Stack Exchange. [27 December 2015]. (原始內容存檔於2020-08-09). 

外部連結