線程局部存儲TLS
1 .使用線程局部存儲的理由
當我們希望這個進程的全局變量變為線程私有時,而不是所有線程共享的,也就是每個線程擁有一份副本時,這時候就可以用到線程局部存儲(TLS,Thread Local Storage)這個機制了。
2.動態TLS
(1)調用TlsAlloc函數
兩種方式:
1>全局調用一次: global_dwTLSindex=TLSAlloc();
如果程序只調用一次TlsAlloc,而不是每個線程都調用一次TlsAlloc()函數的話,多個線程就使用同一個索引值,不同線程雖然看起來用的是同名的TLS數組索引變量,但實際上各個線程得到的可能是 不同DWORD值。其意義在於,每個使用TLS的線程獲得了一個DWORD類型的線程局部靜態變量作為TLS數組的索引變量
2>進程內的每個線程都調用一次:
每個線程得到不一樣的索引值,調試可以看到每一次調用,索引值的大小是自加1而遞增的。
函數流程:
1>該函數會檢索系統進程中的位標誌並找到一個FREE標誌,然後將該標誌從FREE改為INUSE,並返回該標誌在位數組中的索引,通常將該索引保存在一個全局變量中,因為這個值會在整個進程範圍內(而不是線程範圍內)使用。
2>如果TlsAlloc無法在列表中找到一個FREE標誌,會返回TLS_OUT_OF_INDEXES。
3>TlsAlloc函數在函數返回之前,會遍歷進程中的每個線程,並根據新分配的索引,在每個線程的Tls數組中把對應的元素設為0
(2)調用TlsSetValue(dwTlsIndex,pvTlsValue)
將一個值放到線程的數組中
該函數把pvTlsValue所標誌的一個PVOID值放到線程的數組中,dwTlsIndex指定了在數組中的具體位置(由TlsAlloc得到)
當一個線程調用TlsSetValue的時候,會修改自己的數組。但它無法修改另一個線程的TLS數組中的值。
(3)調用PVOID TlsGetValue(dwTlsIndex)
從線程的數組中取回一個值
與TlsSetValue相似,TlsGetValue中會查看屬於調用線程的數組
(4)調用TlsFree(dwTlsIndex)
釋放己經預訂的TLS元素
該函數會將進程內的位標誌數組對應的INUSE標誌重設回FREE
同時該函數還會將所有線程中該元素的內容設為0。
試圖釋放一個尚未分配的TLS元素將導致錯誤
// DynamicTLS.cpp : 定義控制臺應用程序的入口點。 // #include "stdafx.h" #include <windows.h> #include <process.h> #include <ctime> #include <cstdlib> #include <vector> #include <iostream> using namespace std; clock_t gc_begin = 0; clock_t gc_End = 0; clock_t gc_Interval = 0; //全局 //unsigned long __stdcall threadProc(void *arg); CreateThread unsigned int __stdcall ThreadProc(void *arg); int main(int argc, char *argv[]) { /* 不同線程雖然看起來用的是同名的TLS數組索引變量,但實際上各個線程得到的可能是不同DWORD值。 其意義在於,每個使用TLS的線程獲得了一個DWORD類型的線程局部靜態變量作為TLS數組的索引變量。 */ //這裏使用 TlsAlloca() 可以,在線程內部調用這個函數也可以,使得每個線程都有一個不同的索引值, //僅僅在這裏調用一次,程序的實現也成功了,不同線程使用了同一個索引值也成功了 //DWORD tlsIndex = TlsAlloc(); //此步之後,當前線程實際上訪問的是這個TLS數組索引變量的線程內的拷貝版本 //這裏調用 TlsAlloc()產生的第一個索引值也是1 DWORD tlsIndex = 0; vector<HANDLE> threads; for (int i = 0; i < 2; ++i) { HANDLE h = (HANDLE)_beginthreadex(NULL, 0, ThreadProc, (void*)tlsIndex, 0, NULL); /* 使用_beginthreadex 和 CreateThread 創建線程產生的第一個索引值TlsAlloc()不同, CreateThread是 1,_beginthreadex產生的是3,占用了兩個索引位? */ //HANDLE h = (HANDLE)CreateThread(NULL, 0, ThreadProc, //(void*)tlsIndex, 0, NULL); Sleep(2000); threads.push_back(h); } for (size_t i = 0; i < threads.size(); ++i) { WaitForSingleObject(threads[i], INFINITE); CloseHandle(threads[i]); } } // unsigned int __stdcall ThreadProc(void *arg) { DWORD tlsIndex = TlsAlloc(); //DWORD tlsIndex = reinterpret_cast<DWORD>(arg); gc_begin = clock(); //從“開啟這個程序進程”到“程序中調用clock()函數”時之間的CPU時鐘計時單元(clock tick)數 TlsSetValue(tlsIndex, PVOID(gc_begin)); // 利用TlsSetValue 設置 值 printf("Thread ID: %d, Thread Begin Time: %d\n", GetCurrentThreadId(), gc_begin); Sleep(2000); gc_End = clock(); gc_Interval = gc_End - reinterpret_cast<clock_t>(TlsGetValue(tlsIndex)); double sec = 1.0 * gc_Interval / CLOCKS_PER_SEC; // 利用TlsGetValue取得值 printf("Thread ID: %d, Thread End Time: %d, Survival Time: %f\n", GetCurrentThreadId(), gc_End, sec); return 0; }
這裏補充一下_beginthreadex()函數和CreateThread函數在創建線程時候,所創建的線程,調用TlsAlloc函數產生的索引值的不同,_beginthreadex()函數下的線程第一個索引值是3,而CreateThread函數下的線程第一個索引值是1,應該是_beginthreadex()函數下的線程出於某種原因占用了兩位:
CreateThread函數下的線程第一個索引值:
_beginthreadex()函數下的線程第一個索引值:
3.靜態TLS
(1)靜態TLS變量的聲明
__declspec(thread) int number;
(2)靜態TLS的實現原理
對於Windows系統來說,正常情況下一個全局變量或靜態變量會被放到".data"或".bss"段中,但當我們使用__declspec(thread)定義一個線程私有變量的時候,編譯器會把這些變量放到PE文件的".tls"段中。
當系統啟動一個新的線程時,它會從進程的堆中分配一塊足夠大小的空間,然後把".tls"段中的內容復制到這塊空間中,於是每個線程都有自己獨立的一個".tls"副本。所以對於用__declspec(thread)定義的同一個變量,它們在不同線程中的地址都是不一樣的。
線程環境塊(TEB,Thread Environment Block)。這個結構裏面保存的是線程的堆棧地址、線程ID等相關信息,其中有一個域是一個TLS數組,它在TEB中的偏移是0x2C。對於每個線程來說,x86的FS段寄存器所指的段就是該線程的TEB,於是要得到一個線程的TLS數組的地址就可以通過FS:[0x2C]訪問到。
// Static_TLS.cpp : 定義控制臺應用程序的入口點。 // #include "stdafx.h" #include <windows.h> #include <iostream> // 定義靜態TLS全局變量 __declspec(thread) int __TlsValue = 0; using namespace std; DWORD WINAPI ThreadProcedure(LPVOID ParameterData); int main() { // 設置主線程靜態TLS的value為5 __TlsValue = 5; HANDLE ThreadHandle = CreateThread(NULL, 0, ThreadProcedure, NULL, 0, NULL); if (ThreadHandle) { // 等待直到子線程結束 WaitForSingleObject(ThreadHandle, INFINITE); // 取得主線程靜態TLS的值 cout << "主線程 __TlsValue=" << __TlsValue << endl; } return 0; } DWORD WINAPI ThreadProcedure(LPVOID ParameterData) { // 設置子線程value為10,並不影響主線程 __TlsValue = 10; // 取得子線程靜態TLS的值 cout << "子線程 __TlsValue=" << __TlsValue << endl; return 0; }
線程局部存儲TLS