线程局部存储区

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变量在线程中的地址。

以上是关于线程局部存储区的主要内容,如果未能解决你的问题,请参考以下文章

JVM理论:(三/7)关于类变量成员变量局部变量的案例总结

Java的代码运行的内存分析

C++中,静态数组在内存中是存储在堆上,还是栈上,还是在静态存储区中?

面向对象内存分析

C/C++程序中全局变量局部变量堆栈的存储区域介绍

全局变量 静态变量 局部变量 啥时候创建 啥时候撤销