线程局部存储区
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程局部存储区相关的知识,希望对你有一定的参考价值。
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变量在线程中的地址。
以上是关于线程局部存储区的主要内容,如果未能解决你的问题,请参考以下文章