回炉重造之重读Windows核心编程-021-线程本地存储

第21章 线程本地存储

21.0 简介

将数据和对象的实例联系起来是很有用的,比如C库函数strtok。当strtok被一个线程调用之后,又被另一个线程抢占调用了一次,第二个线程的调用就会发生错误。而且这种错误是很难被发现和排除的。

由于CC++运行时库出现的时候大多数应用是基于单线程的应用程序的,并不会考虑这个。为了解决这个问题,运行时库就需要给每一个线程都配备一个单独的字符串指针,就不会发生这样的错误了,像strtok、asctime、gmtime这类的函数就不会有上述的问题了。

解决这个问题的特性就是线程本地存储(Thread Local Storage, TLS)。这个特性同样可以让你的线程减少对静态变量和全局变量的使用。和线程联系起来意味着线程的生命周期结束时,这些资源也有机会被销毁。

在编写应用程序的时候,有2种使用TLS的方法,就是动态TLS和静态TLS。

21.1 动态TLS

若要使用动态的TLS,可以调用一组函数(4个)。这些函数实际上是DLL使用的最多。系统中的线程会使用一组标志,可以是FREE或INUSE,表示是否在使用。从Win2K开始,系统就保证有TLS_MINIMUN_AVAILABLE(2000)个内存块可是使用。

21.1.1动态TLS

若要使用动态TLS,首先调用一个函数:

DWORD TlsAlloc();

这个函数对进程中的位标志进行扫描,并找出第一个FREE标志。然后将这个标志从FREE改成INUSE,最后返回一个索引。这个索引被保存在一个全局变量中。

至于剩下的1%:当创建一个线程时,便分配一个TLS_MINIMUM_AVAILABLEPVOID值的数组,并将其初始化为0,由系统将它和线程关联起来。这样每个线程都可以得到自己的数组,数组中的PVOID可以存储任何值。

这样设计的方式体现出来,线程可以使用TlsAlloc特意保留的一个索引。若要将一个值放入线程的数组中,可以调用TlsValue函数。

BOOL TlsSetValue(
	DWORD dwTlsIndex,
  PVOID pvTlsValue);

这个函数将一个PVOID值(pvTlsValue参数)放入线程的数组中由dwTlsIndex参数表示的索引处。pvTlsValue的值与调用TlsSetValue的线程想联系。如果调用成功,就返回TRUE。

线程调用TlsSetValue的时候只改变自己的数组,不会对其他线程的数组造成影响。如果想要对其他线程发送一些数据,可以在调用CreateThread或者_beginthreadex,然后还函数将信息作为线程函数的参数传递给CreateThread或者_begingthreadex函数。

考虑到可能有多次TlsAlloc函数的调用,则当调用TlsSetValue之时始终都应该有较早时候调用TlsAlloc函数返回的索引。对于这些函数Microsoft设计成可以尽快运行,放弃错误检查。

若要从线程的数组中检索出一个数组的索引,可以调用TlGetValue:

PVOID TlsGetValue(DWORD dwTlsIndex);

当在所有线程中的不再需要保留TLS时隙的时候,应该调用TlsFree:

BOOL TlsFree(DWORD dwTlsIndex);

这个函数简单地告诉系统这个时隙不再需要保留。那么由进程管理的位标志数组中对应的标志就从INUSE变成了FREE。这个函数如果运行成功就返回TRUE,如果失败讲返回一个错误。

21.1.2 使用动态TLS

如果DLL也使用TLS,那么当它用DLL_PROCESS_ATTACH标志调用DllMain函数的时候,必然也调用了TlsAlloc;当它用DLL_PROCESS_DETACH标志调用DllMain函数的时候,当然也调用了TlsFree。对于TlsSetValue和TlsGetValue也可能在DLL已经实现的函数中被调用。

使用TLS的方法之一是当应用程序需要的时候添加给应用程序。如下面的代码所示:

DWORD g_dwTlsIndex; // Assume that this is initialized 
									// with the result of a call to TlsAlloc.
// ...
void MyFunction(PSOMESTRUCT pSomeStruct) {
  if (pSomeStrunct != NULL) {
    // The caller is priming this function
    // See if we already allocated space to save the data.
    if(TlsGetValue(g_dwTlsIndex) == NULL) {
      // Space was never allocated. This is the first 
      // time this function has ever been called by this thread.
      TlsSetValue(g_dwTlsIndex, 
                  HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct)));
    }
    // Memory already exists for the data;
    // save the newly passed values.
    memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct));
  } else {
    // The call already primed the function. Now it 
    // wants to do something with the saved data.
    
    // Get the address of the saved data.
    pSomeStruct = (PSOMESTRUCT) TlsGetValue(g_dwTlsIndex);
    
    // The saved data is pointed to by pSomeStruct; use it.
    // ...
  }
}

如果上面的程序没有没调用过,也就不必为线程分配内存块了。

事实上每个DLL可能或多或少地用到TLS,为此最好的做法是在TLS中保存内存块的指针,而不是把整个内存卡放进去。Windows2000以后可以设置1000多个TLS,像上面的方式节省地使用仍然是比较好的策略。

关于TlsAlloc函数,还有一些值得一提的功能:

DWORD dwTlsIndex;
PVOID pvSomeValue;
// ...
dwTlsIndex = TlsAlloc();
TlsSetValue(dwTlsIndex, (PVOID) 12345);
TlsFree(dwTlsIndex);
// ...
// Assume that the dwTlsIndex value returned from
// this call to TlsAlloc is identical to the index 
// return by the earlier call to TlsAlloc.
dwTlsIndex = TlsAlloc();
pvSomeValue = TlsGetValue(dwTlsIndex);

第十一行的的函数的返回值已经不是12345,而是0。因为TlsAlloc在返回之前,要遍历进程中的每个线程,在每个线程的数组中的新分配索引处放入0。这是因为也许已经有新的DLL被线程加载到地址空间中,那么线程如果调用TlsAlloc,获得的数组的首地址上的元素就是原来的12345了,线程也许会因为这个而无法运行。

21.2 静态TLS

与动态TLS相同,静态的TLS也可以将数据与线程联系起来。不过相比之下静态TLS要简单得多,不必调用任何函数就可以做到。例如你想将起始时间与应用程序创建的每个线程联系起来,只需要将其实时间变量声明为下面的形式:

__declspec(thread) DWORD gt_dwStartTime = 0;

__declspec(thread)这个前缀是Microsoft添加给Visual C++编译器的一个修饰符,它告诉编译器被修饰的变量将放入可执行文件或DLL文件中它自己的节中。__declspec(thread)修饰的变量必须是一个静态的或者全局的变量。当上面的代码被编译之后,编译器会将所有的TLS变量放入程序自己的节中,这个节就是.tls。

为了使得静态TLS能够运行,操作系统将搜索你的可执行文件中的.tls节,并分配一块足够大的内存来存放它。而每当你的应用程序使用静态TLS中的其中一个变量之时,必须将其转化为应用程序中的内存中的位置,因此编译器会有一些代码来做这些工作。所以TLS的数目也是对应用程序的性能的考验。在x86CPU上,将为每次引用的静态TLS变量生成3个辅助机器指令。

新线程都会有一块新的内存块,以存放线程的静态TLS变量。每个线程也都对自己的静态TLS变量有访问权限,不能访问属于其他线程的TLS变量。

如果应用程序和DLL都需要使用静态的TLS变量,那么系统在加载应用程序的时候首先会确定tls节的大小,并将DLL中的可能存在的tls节的大小相加。然后当在进程中创建线程的时候,系统自动分配足够大的内存块来存放所有TLS变量。

当系统调用LoadLibrary的时候,系统会产看进程中已经存在的线程,并扩大它们的内存块,以便适应新的DLL对内存的需求。而如果调用FreeLibrary释放包含静态TLS变量的DLL的时候,被扩大的内存也将被压缩。

这样的任务对于系统来说,有些重了。虽然系统允许包含静态TLS变量的库在运行时显示加载,但是TLS数据不会被初始化。如果这些数据被访问,有可能出现访问违规。相比之下,使用动态的TLS就不会有这样的问题,可以在运行时释放,很灵活不会产生问题。

原文地址:https://www.cnblogs.com/leoTsou/p/13711493.html