理解线程局部存储区

线程局部存储区,英文总称Thread local Storage,简TLS。

那TLS有什么用处呢?起始我们可以使用TLS将数据于一个正在运行的线程关联起来。将数据与线程关联起来是有帮助的,比如我们使用TLS来确定线程运行了多长时间。

C/C++的运行库也使用了TLS,但是C/C++运行库是在多线程程序出现的很多年前设计的,所以运行库中的很多函数都是不支持多线程的,所以在多线程的环境中使用运行库中的函数会发生各种各样的错误与缺陷。

那大神们是如何解决这个问题的呢?是的,C/C++运行库的解决方案就是使用TLS。

如果程序高度依赖全局变量或者静态变量,那么TLS将会很有用处。

TLS可以分为动态TLS和静态TLS,都是用在DLL上比较多。

动态TLS:

我们的程序可以调用四个函数来使用动态TLS,这些函数经常在DLL上被调用。

我们先来描述一下Microsoft Windows管理TLS的内部数据结构,系统中每一个进程都有一组正在使用的标志(in-use flag),而每一个标志都可以被设置为true或者false来表示TLS元素是不是正在使用,Microsoft保证了至少有TLS_MINIMUM_AVAILABLE(被定义为64)个标志可以使用,但是系统会在需要的时候分配更多的TLS。

我们要使用动态TLS就必须先调用TlsAlloc函数,

函数原型  DWORD TlsAlloc();

这个函数会让系统对进程中的标志进行检索并找到一个FREE标志,如何系统会将该标志从FREE改为INUSE,函数TlsAlloc会返回该标志的索引(我们通常将这个索引保存到全局变量中)。

然而函数TlsAlloc无法找到一个FREE标志,那么函数就会返回TLS_OUT_OF_INDEXES(被定义为0xffffffff),而查找成功就会返回0。

在系统创建一个线程的时候会分配TLS_MINIMUM_AVAILABLE哥PVOID指,然后将它们初始化为0并与该线程关联起来,所以每个线程都有自己的PVOID数组,可以保存任意指。

我们要将数据保存到线程的PVOID数组前必须要知道哪一个索引的PVOID可以使用,那我们怎么找索引呢?就是前面TlsAlloc函数的作用了,该函数帮我们预定空闲的PVOID并返回该PVOID的索引供我们存放数据,被我们预定了的PVOID,该进程的其它线程不可以用该PVOID,因为它的标志被设置为INSER了。

接下来为了把数据存放到该PVOID中,我们需要调用TlsSetValue函数,

函数原型  BOOL TlsSetValue(DWORD dwTlsIndex,PVOID pvTlsValue);

第一个参数dwTlsIndex就是前面函数TlsAlloc获得的索引值,而第二个参数就是指向数据的一个指针,pvTlsValue值会与调用TlsSetValue的线程关联起来,成功便会返回true。

注意一个线程调用TlsSetValue函数是不可以修改另外一个线程的TLS,而Microsoft为了这些函数运行尽可能快,在实现这些函数的时候没有加入错误检查,所以说传入的索引值不是由函数TlsAlloc得到的,系统仍然会将数据传人指定索引的PVOID中。

为了从线程中的数据我们应该使用函数TlsGetValue()。

函数原型   PVOID TlsGetValue(DWORD dwTlsIndex);

这个函数的参数就是索引,想获取哪个索引的PVOID数据就传人那个索引,返回的就是PVOID数据。相似的是函数TlsGetValue也是只会查看属于调用线程的数据索引。

当我们不需要一个TLS数据的时候,我们应该调用TlsFree()函数,

函数原型   BOOL TlsFree(DWORD dwTlsIndex);

这个函数的参数还是那个索引,告诉系统这个索引的PVOID数据已经不需要了,函数会将进程内的指定索引标志从INUSE设置回FREE,调用成功就会返回true,然而试图释放一个索引标志为FREE的将会导致错误。

通常在DLL中使用TLS,那么将会在DllMain函数处理DLL_PROCESS_ATTACH的时候调用TlsAlloc函数,而在DllMain函数处理DLL_PROCESS_DETACH的时候调用TlsFree。

静态TLS:

与动态TLS一样,静态TLS也是将数据与线程关联起来,但是在使用的时候不需要调用任何其它函数。假设要将程序创建的每个线程与线程的启动时间关联起来,我们就需要这样子声明

_declspec(thread) DWORD g_dwStartTime=0;

解释一下,_declspec(thread)前缀是Microsoft为Visual C++编译器增加的一个修饰符,它告诉编译器应该在程序或者DLL中把对应的变量放在它自己的段中,该修饰符后面的变量必须被声明为全局变量或者静态变量,我们不能将局部变量声明为_declspec(thread)。

在编译器对程序进行编译的时候会将所有的TLS变量放到它们自己的段中,这个段命叫.tls,连接器会将所有对象模块中的.tls段合并成一个大的.tls段。为了让TLS能够正常工作,操作系统也要参与进来,当系统载入程序到内存的时候会查看可执行文件的.tls段,然后分配一块足够大的内存来保存所有的TLS变量。一系列的操作都需要编译器生成额外的代码所以导致程序的体积更大而且执行更慢,在x86CPU上每引用一个静态TLS变量都会生成额外三条机器指令。

 

你可能感兴趣的:(Window核心)