21.1 动态TLS
21.1.1 为什么要使用线程局部存储
编写多线程程序的时候都希望存储一些线程私有的数据,我们知道,属于每个线程私有的数据包括线程的栈和当前的寄存器,但是这两种存储都是非常不可靠的,栈会在每个函数退出和进入的时候被改变,而寄存器更是少得可怜。假设我们要在线程中使用一个全局变量,但希望这个全局变量是线程私有的,而不是所有线程共享的,该怎么办呢?这时候就须要用到线程局部存储(TLS,Thread Local Storage)这个机制了。
21.1.2 动态TLS
(1)每个进程都有一组正在使用标志,共TLS_MINIMUM_AVAILABLE个。每个标志可以被设为FREE或INUSE,表示该TLS元素是否正在使用。(注意这组标志属进程所有)
(2)当系统创建一个线程的时候,会为该线程分配与线程关联的、属于线程自己的PVOID型数组(共有TLS_MINIMUM_AVAILBALE个元素),数组中的每个PVOID可以保存任意值。
21.1.3 使用动态TLS
(1)调用TlsAlloc函数
①该函数会检索系统进程中的位标志并找到一个FREE标志,然后将该标志从FREE改为INUSE,并返回该标志在位数组中的索引,通常将该索引保存在一个全局变量中,因为这个值会在整个进程范围内(而不是线程范围内)使用。
②如果TlsAlloc无法在列表中找到一个FREE标志,会返回TLS_OUT_OF_INDEXES。
③以上就是TlsAlloc99%的工作,剩1%的工作就是在函数返回之前,会遍历进程中的每个线程,并根据新分配的索引,在每个线程的Tls数组中把对应的素素设为0(具体原因请看后面的分析)。
(2)调用TlsSetValue(dwTlsIndex,pvTlsValue)将一个值放到线程的数组中
①该函数把pvTlsValue所标志的一个PVOID值放到线程的数组中,dwTlsIndex指定了在数组中的具体位置(由TlsAlloc得到)
②当一个线程调用TlsSetValue的时候,会修改自己的数组。但它无法修改另一个线程的TLS数组中的值。
(3)从线程的数组中取回一个值:PVOID TlsGetValue(dwTlsIndex)
①与TlsSetValue相似,TlsGetValue中会查看属于调用线程的数组
(4)释放己经预订的TLS元素:TlsFree(dwTlsIndex)
①该函数会将进程内的位标志数组对应的INUSE标志重设回FREE
②同时该函数还会将所有线程中该元素的内容设为0。
③试图释放一个尚未分配的TLS元素将导致错误
21.1.4编写类似_tcstok_s函数
DWORD g_dwTlsIndex; //假设这个全局变量是通过TlsAlloc函数来初始化的 void MyFunction(PSOMESTRUCT PSomeStruct){ if (pSomeStruct != NULL){ //调用者正在启用该函数,就像strok函数第一次传入非NULL, //以后传为NULL //检查是否己经为数据分配存储空间 if (TlsGetValue(g_dwTlsIndex) == NULL) //线程第一次调用该函数时,该空间尚未分配 //TlsAlloc函数返回之前,会将进程中所有线程的g_dwTlsIndex元素 //清零,以保证这句代码不会出错非空的现象! //通过TLS能保证分配的空间只与调用线程相关联 TlsSetValue(g_dwTlsIndex, HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct)); } //将传入的pSomeStruct数据保存刚才那个只与调用线程相关的存储空间中 memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct)); } else{ //调用者己经第二次(或以上)调用该函数,会传入NULL参数 //取出数据 pSomeStruct = (PSOMESTRUCT)TlsGetValue(g_dwTlsIndex); //以下可以开始pSomeStruct这个数据了。 ... } }
21.2 静态TLS
(1)静态TLS变量的声明
①__thread int number;//GCC使用__thread关键字声明
②__declspec(thread) int number; //MSVC使用__declspec(thread)声明
(2)Windows中静态TLS的实现原理
①对于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"段中。
⑤另外一个问题是,既然同一个TLS变量对于每个线程来说它们的地址都不一样,那么线程是如何访问这些变量的呢?其实对于每个Windows线程来说,系统都会建立一个关于线程信息的结构,叫做线程环境块(TEB,Thread Environment Block)。这个结构里面保存的是线程的堆栈地址、线程ID等相关信息,其中有一个域是一个TLS数组,它在TEB中的偏移是0x2C。对于每个线程来说,x86的FS段寄存器所指的段就是该线程的TEB,于是要得到一个线程的TLS数组的地址就可以通过FS:[0x2C]访问到。
⑥这个TLS数组对于每个线程来说大小是固定的,一般有64个元素。而TLS数组的第一个元素就是指向该线程的".tls"副本的地址。于是要得到一个TLS的变量地址的步骤为:首先通过FS:[0x2C]得到TLS数组的地址,然后根据TLS数组的地址得到".tls"副本的地址,然后加上变量在".tls"段中的偏移即该TLS变量在线程中的地址。
【DllTls】演示线程局部存储的例子
1、动态链接库端的代码:
/************************************************************************ Module: DllTls.h ************************************************************************/ #pragma once #include <windows.h> #ifdef DLLTLS_EXPORT //DLLTLS_EXPORT必须在Dll源文件包含该头件前被定义 #define DLLTLSAPI extern "C" __declspec(dllexport) //本例中所有的函数和变量都会被导出 #else #define DLLTLSAPI extern "C" __declspec(dllimport) #endif //定义要导出的函数的原型 DLLTLSAPI VOID* GetThreadBuf(); DLLTLSAPI UINT GetThreadBufSize();
#include <tchar.h> #include <windows.h> #include <strsafe.h> #include <locale.h> //在这个DLL源文件定义要导出的函数和变量 #define DLLTLS_EXPORT //这个源文件中须定义这个宏,以告诉编译器函数要 //__declspect(dllexport),这个宏须在包含MyLib.h //之前被定义 #include "DllTls.h" #define GRS_ALLOC(sz) HeapAlloc(GetProcessHeap(),0,sz) #define GRS_CALLOC(sz) HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sz) #define GRS_SAFEFREE(p) if(NULL!=p){HeapFree(GetProcessHeap(),0,p);p=NULL;} static DWORD g_dwTLS = 0; static UINT g_nBufSize = 256; BOOL APIENTRY DllMain(HANDLE hDllHandle, DWORD dwReason, LPVOID lpreserved){ switch (dwReason) { case DLL_PROCESS_ATTACH: { _tsetlocale(LC_ALL, _T("chs")); g_dwTLS = TlsAlloc(); if (TLS_OUT_OF_INDEXES == g_dwTLS){ _tprintf(_T("为进程[ID:0x%X]分配TLS索引失败! "), GetCurrentProcessId()); return FALSE; } _tprintf(_T("为进程[ID:0x%X]分配TLS索引:%u! "), GetCurrentProcessId(),g_dwTLS); } break; case DLL_THREAD_ATTACH: { PVOID pThdData = GRS_CALLOC(g_nBufSize); if (!TlsSetValue(g_dwTLS,pThdData)){ _tprintf(_T("为线程[ID:0x%X]设置TLS索引[%u]变量[0x%08X]失败! "), GetCurrentThreadId(), g_dwTLS,pThdData); GRS_SAFEFREE(pThdData); return FALSE; } _tprintf(_T("为线程[ID:0x%X]设置TLS索引[%u]变量[0x%08X]! "), GetCurrentThreadId(), g_dwTLS, pThdData); } break; case DLL_THREAD_DETACH: { PVOID pThdData = TlsGetValue(g_dwTLS); if (!pThdData){ _tprintf(_T("为线程[ID:0x%X]获取TLS索引[%u]变量[0x%08X]失败! "), GetCurrentThreadId(), g_dwTLS, pThdData); return FALSE; } _tprintf(_T("为线程[ID:0x%X]获取TLS索引[%u]变量[0x%08X]并销毁! "), GetCurrentThreadId(), g_dwTLS, pThdData); GRS_SAFEFREE(pThdData); } break; case DLL_PROCESS_DETACH: { _tprintf(_T("释放进程[ID:0x%X]TLS索引[%u] "), GetCurrentProcessId(), g_dwTLS); TlsFree(g_dwTLS); } break; } return TRUE; } VOID* GetThreadBuf() { return TlsGetValue(g_dwTLS); } UINT GetThreadBufSize(){ return g_nBufSize; }
2、测试程序
#include <windows.h> #include <tchar.h> #include <strsafe.h> #include <locale.h> #include "../../Chap21/21_DLLTls/DllTls.h" #pragma comment(lib,"../../Debug/21_DLLTls.lib") #define GRS_CREATETHREAD(Fun,Param) CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Fun,Param,0,NULL) DWORD WINAPI ThreadProc(LPVOID pvParam){ TCHAR* pTlsBuf = (TCHAR*)GetThreadBuf(); UINT nSize = GetThreadBufSize(); if (NULL !=pTlsBuf && 0 != nSize){ _tprintf(_T("线程[0x%X]取得缓冲区[地址:0x%08X 大小(字节):%u],写入数据 "), GetCurrentThreadId(),pTlsBuf,nSize); StringCchPrintf(pTlsBuf, nSize / sizeof(TCHAR), _T("ID:0x%X"), GetCurrentThreadId()); Sleep(1000); _tprintf(_T("线程[0x%X]取得缓冲区[地址:0x%08X 大小(字节):%u],写入的数据为[%s] "), GetCurrentThreadId(), pTlsBuf, nSize,pTlsBuf); } _tprintf(_T("线程[0x%X]退出 "),GetCurrentThreadId()); return 0; } #define THREADCNT 2 int _tmain(){ _tsetlocale(LC_ALL, _T("chs")); HANDLE phThread[THREADCNT] = {}; for (int i = 0; i < THREADCNT; i++){ phThread[i] = GRS_CREATETHREAD(ThreadProc, NULL); } WaitForMultipleObjects(THREADCNT, phThread, TRUE, INFINITE); for (int i = 0; i < THREADCNT;i++){ CloseHandle(phThread[i]); } _tsystem(_T("PAUSE")); return 0; }