线程局部存储实现
来源:互联网 发布:淘宝客佣金链接 编辑:程序博客网 时间:2024/06/08 16:07
《程序员的自我修养:链接、装载与库》第11章运行库。本章主要介绍运行库的概念、C/C++运行库、Glibc和MSVC CRT、运行库如何实现C++全局构造和析构及以fread()库函数为例对运行库进行剖析。本节为大家介绍线程局部存储实现。
11.3.3 线程局部存储实现(1)
很多时候,开发者在编写多线程程序的时候都希望存储一些线程私有的数据。我们知道,属于每个线程私有的数据包括线程的栈和当前的寄存器,但是这两种存储都是非常不可靠的,栈会在每个函数退出和进入的时候被改变;而寄存器更是少得可怜,我们不可能拿寄存器去存储所需要的数据。假设我们要在线程中使用一个全局变量,但希望这个全局变量是线程私有的,而不是所有线程共享的,该怎么办呢?这时候就须要用到线程局部存储(TLS,Thread Local Storage)这个机制了。TLS的用法很简单,如果要定义一个全局变量为TLS类型的,只需要在它定义前加上相应的关键字即可。对于GCC来说,这个关键字就是__thread,比如我们定义一个TLS的全局整型变量:
__thread int number;对于MSVC来说,相应的关键字为__declspec(thread):
__declspec(thread) int number;
在Windows Vista和2008之前的操作系统,如果TLS的全局变量被定义在一个DLL中,并且该DLL是使用LoadLibrary()显式装载的,那么该全局变量将无法使用,如果访问该全局变量将会导致程序发生保护错误。导致这个情况的主要原因是在Windows Vista之前的操作系统下,DLL在使用LoadLibrary()装载时无法正确初始化由__declspec(thread)定义的变量,具体请参照MSDN。
一旦一个全局变量被定义成TLS类型的,那么每个线程都会拥有这个变量的一个副本,任何线程对该变量的修改都不会影响其他线程中该变量的副本。
Windows TLS的实现
对于Windows系统来说,正常情况下一个全局变量或静态变量会被放到".data"或".bss"段中,但当我们使用__declspec(thread)定义一个线程私有变量的时候,编译器会把这些变量放到PE文件的".tls"段中。当系统启动一个新的线程时,它会从进程的堆中分配一块足够大小的空间,然后把".tls"段中的内容复制到这块空间中,于是每个线程都有自己独立的一个".tls"副本。所以对于用__declspec(thread)定义的同一个变量,它们在不同线程中的地址都是不一样的。
我们知道对于一个TLS变量来说,它有可能是一个C++的全局对象,那么每个线程在启动时不仅仅是复制".tls"的内容那么简单,还需要把这些TLS对象初始化,必须逐个地调用它们的全局构造函数,而且当线程退出时,还要逐个地将它们析构,正如普通的全局对象在进程启动和退出时都要构造、析构一样。
Windows PE文件的结构中有个叫数据目录的结构,我们在第2部分已经介绍过了。它总共有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]访问到。
TEB这个结构不是公开的,它可能随着Windows版本的变化而变化,我们这里所说的TEB结构都是指在x86版的Windows XP。
这个TLS数组对于每个线程来说大小是固定的,一般有64个元素。而TLS数组的第一个元素就是指向该线程的".tls"副本的地址。于是要得到一个TLS的变量地址的步骤为:首先通过FS:[0x2C]得到TLS数组的地址,然后根据TLS数组的地址得到".tls"副本的地址,然后加上变量在".tls"段中的偏移即该TLS变量在线程中的地址。下面看一个简单的例子:
__declspec(thread) int t = 1;
int main(){t = 2;return 0;}经过编译以后,这段代码的汇编实现如下:
_main:00000000: 55 push ebp00000001: 8B EC mov ebp,esp00000003: A1 00 00 00 00 mov eax,dword ptr [__tls_index]00000008: 64 8B 0D 00 00 00 mov ecx,dword ptr fs:[__tls_array]000000000F: 8B 14 81 mov edx,dword ptr [ecx+eax*4]00000012: C7 82 00 00 00 00 mov dword ptr _t[edx],202 00 00 000000001C: 33 C0 xor eax,eax0000001E: 5D pop ebp0000001F: C3 ret
代码中有两个符号__tls_index和__tls_array,它们被定义在MSVC CRT中,对于MSVC 2008来说,它们的值分别是0和0x2C,分别表示TLS数组下的第一个元素和TLS数组在TEB中的偏移。由于这两个数值有可能随着Windows系统的变化而变化,所以它们被保存在CRT中,如果程序以DLL方式链接,那么在不同版本的Windows平台上运行就不会有问题;如果是静态链接,那么当新版的Windows更改TEB结构时而导致TLS数组在TEB中的偏移改变,程序运行就可能出错。当然出于Windows多年来的"良好表现",这种随意更改核心数据结构的事情发生的可能性还是比较小的。
显式TLS
前面提到的使用__thread或__declspec(thread)关键字定义全局变量为TLS变量的方法往往被称为隐式TLS,即程序员无须关心TLS变量的申请、分配赋值和释放,编译器、运行库还有操作系统已经将这一切悄悄处理妥当了。在程序员看来,TLS全局变量就是线程私有的全局变量。相对于隐式TLS,还有一种叫做显式TLS的方法,这种方法是程序员须要手工申请TLS变量,并且每次访问该变量时都要调用相应的函数得到变量的地址,并且在访问完成之后需要释放该变量。在Windows平台上,系统提供了TlsAlloc()、TlsGetValue()、TlsSetValue()和TlsFree()这4个API函数用于显式TLS变量的申请、取值、赋值和释放;Linux下相对应的库函数为pthread库中的pthread_key_create()、pthread_getspecific()、pthread_setspecific()和pthread_key_delete()。
显式的TLS实现其实非常简单,我们前面提到过TEB结构中有个TLS数组。实际上显式的TLS就是使用这个数组保存TLS数据的。由于TLS数组的元素数量固定,一般是64个,于是显式TLS在实现时如果发现该数组已经被使用完了,就会额外申请4096个字节作为二级TLS数组,使得在WindowsXP下最多能拥有1088(1024+64)个显式TLS变量(当然隐式的TLS也会占用TLS数组)。相对于隐式的TLS变量,显式的TLS变量的使用十分麻烦,而且有诸多限制,显式TLS的诸多缺点已经使得它越来越不受欢迎了,我们并不推荐使用它。
Q&A: CreateThread()和_beginthread()有什么不同
我们知道在Windows下创建一个线程的方法有两种,一种就是调用Windows API CreateThread()来创建线程;另外一种就是调用MSVC CRT的函数_beginthread()或_beginthreadex()来创建线程。相应的退出线程也有两个函数Windows API的ExitThread()和CRT的_endthread()。这两套函数都是用来创建和退出线程的,它们有什么区别呢?
很多开发者不清楚这两者之间的关系,他们随意选一个函数来用,发现也没有什么大问题,于是就忙于解决更为紧迫的任务去了,而没有对它们进行深究。等到有一天忽然发现一个程序运行时间很长的时候会有细微的内存泄露,开发者绝对不会想到是因为这两套函数用混的结果。
根据Windows API和MSVC CRT的关系,可以看出来_beginthread()是对CreateThread()的包装,它最终还是调用CreateThread()来创建线程。那么在_beginthread()调用CreateThread()之前做了什么呢?我们可以看一下_beginthread()的源代码,它位于CRT源代码中的thread.c。我们可以发现它在调用CreateThread()之前申请了一个叫_tiddata的结构,然后将这个结构用_initptd()函数初始化之后传递给_beginthread()自己的线程入口函数_threadstart。_threadstart首先把由_beginthread()传过来的_tiddata结构指针保存到线程的显式TLS数组,然后它调用用户的线程入口真正开始线程。在用户线程结束之后,_threadstart()函数调用_endthread()结束线程。并且_threadstart还用__try/__except将用户线程入口函数包起来,用于捕获所有未处理的信号,并且将这些信号交给CRT处理。
所以除了信号之外,很明显CRT包装Windows API线程接口的最主要目的就是那个_tiddata。这个线程私有的结构里面保存的是什么呢?我们可以从mtdll.h中找到它的定义,它里面保存的是诸如线程ID、线程句柄、erron、strtok()的前一次调用位置、rand()函数的种子、异常处理等与CRT有关的而且是线程私有的信息。可见MSVC CRT并没有使用我们前面所说的__declspec(thread)这种方式来定义线程私有变量,从而防止库函数在多线程下失效,而是采用在堆上申请一个_tiddata结构,把线程私有变量放在结构内部,由显式TLS保存_tiddata的指针。
了解了这些信息以后,我们应该会想到一个问题,那就是如果我们用CreateThread()创建一个线程然后调用CRT的strtok()函数,按理说应该会出错,因为strtok()所需要的_tiddata并不存在,可是我们好像从来没碰到过这样的问题。查看strtok()函数就会发现,当一开始调用_getptd()去得到线程的_tiddata结构时,这个函数如果发现线程没有申请_tiddata结构,它就会申请这个结构并且负责初始化。于是无论我们调用哪个函数创建线程,都可以安全调用所有需要_tiddata的函数,因为一旦这个结构不存在,它就会被创建出来。
11.3.3 线程局部存储实现(2)
那么_tiddata在什么时候会被释放呢?ExitThread()肯定不会,因为它根本不知道有_tiddata这样一个结构存在,那么很明显是_endthread()释放的,这也正是CRT的做法。不过我们很多时候会发现,即使使用CreateThread()和ExitThread() (不调用ExitThread()直接退出线程函数的效果相同),也不会发现任何内存泄露,这又是为什么呢?经过仔细检查之后,我们发现原来密码在CRT DLL的入口函数DllMain中。我们知道,当一个进程/线程开始或退出的时候,每个DLL的DllMain都会被调用一次,于是动态链接版的CRT就有机会在DllMain中释放线程的_tiddata。可是DllMain只有当CRT是动态链接版的时候才起作用,静态链接CRT是没有DllMain的!这就是造成使用CreateThread()会导致内存泄露的一种情况,在这种情况下,_tiddata在线程结束时无法释放,造成了泄露。我们可以用下面这个小程序来测试:
#include <Windows.h>#include <process.h>
void thread(void *a)
{
char* r = strtok( "aaa", "b" );
ExitThread(0); // 这个函数是否调用都无所谓
}
int main(int argc, char* argv[])
{
while(1) {
CreateThread( 0, 0, (LPTHREAD_START_ROUTINE)thread, 0, 0, 0 );
Sleep( 5 );
}
return 0;
}
如果用动态链接的CRT (/MD,/MDd)就不会有问题,但是,如果使用静态链接CRT (/MT,/MTd),运行程序后在进程管理器中观察它就会发现内存用量不停地上升,但是如果我们把thread()函数中的ExitThread()改成_endthread()就不会有问题,因为_endthread()会将_tiddata()释放。
这个问题可以总结为:当使用CRT时(基本上所有的程序都使用CRT),请尽量使用_beginthread()/_beginthreadex()/_endthread()/_endthreadex()这组函数来创建线程。在MFC中,还有一组类似的函数是AfxBeginThread()和AfxEndThread(),根据上面的原理类推,它是MFC层面的线程包装函数,它们会维护线程与MFC相关的结构,当我们使用MFC类库时,尽量使用它提供的线程包装函数以保证程序运行正确。
相关链接:
http://www.cnblogs.com/zhoug2020/p/3951371.html
http://blog.csdn.net/whatday/article/details/8781640
http://blog.itpub.net/7416120/viewspace-871909/
- 线程局部存储实现
- 线程局部存储实现
- Windows 下实现线程局部存储
- MFC 线程局部存储
- 线程局部存储
- 线程局部存储TLS
- 线程局部存储
- 线程局部存储
- 线程局部存储TLS
- MFC线程局部存储
- TLS--线程局部存储
- 线程局部存储(TLS)
- 线程局部存储
- Java 线程局部存储
- 有关线程局部存储
- 线程局部存储(TLS)
- TLS--线程局部存储
- 线程局部存储
- 【转】java多线程例子
- 斯坦福大学深度学习与自然语言处理第一讲:引言
- JavaScript数字转换为中文的方法
- 最大公约数与最小公倍数(C++)
- Apache traffic server 配置文件records.config的官方文档
- 线程局部存储实现
- ios、Android界面适配,欢迎大家一起补充,探讨
- MD5 Hash + Salt的密码存储方式实现
- 我的Android广告平台选择经历
- jquery中this与$(this)的用法区别.和于js中的this区别
- TWaver自动化设计平台Legolas —— 交互事件响应动作
- 正则表达式 \b \b的用法
- Hashmap
- 简单说说WebHDFS和HttpFS