第二十一章 线程局部存储区
本章内容
21.1 动态TLS
21.2 静态TLS
有时候将数据与一个对象的实力关联起来是有帮助的。比如SetWindowWord SetWindowLong函数将数据和窗口关联起来。
可以使用线程局部存储区(Thread Local Storage TLS)来将数据与一个正在执行的指定线程关联起来。
c/c++运行库使用了TLS. C++运行库为每个线程分配独立的存储空间来存储特定的变量。
作者建议尽量避免使用全局变量。如果应用程序使用了全局变量和静态变量,应该对每个变量进行分析,并研究将其改成栈或堆上的变量。
也可以使用TLS技术--动态TLS和静态TLS,在创建DLL的时候更有用。因为DLL本身并不知道它被连接到的应用程序结构是什么样的。
21.1 动态TLS
应用程序通过调用一组4个函数来使用动态TLS。 以下是TLS的内部数据结构:
系统中每个进程都有一组正在使用的标志(in-use flag)每个标志可以设为
FREE或
INUSE,表示该TLS元素是否正在使用。MS保证至少有
TLS_MINIMUM_AVAILABLE个标志可供使用。
TLS_MINIMUM_AVAILABLE在Winnt.h中被定义为64,实际上最多可达1000多个。
首先需要使用TlsAlloc()
_Must_inspect_result_
WINBASEAPI
DWORD
WINAPI
TlsAlloc(
VOID
);
该函数让系统对进程中的位标志进行检索并找到一个
FREE标志。然后系统将该标志从
FREE改为
INUSE。并让
TlsAlloc返回该标志在数组中的索引。通常在DLL中这个索引保存在一个全局变量中。
如果
TlsAlloc无法在列表中找到一个
FREE标志,那么它会返回
TLS_OUT_OF_INDEXES(在winbase.h被定义为0xFFFFFFFF)
当
TlsAlloc第一次被调用的时候,系统会发现第一个标志为
FREE,于是将该标志改为
INUSE,并让
TlsAlloc返回0(第一条索引)
当系统创建一个线程的时候,会分配
TLS_MINIMUM_AVAILABLE个PVOID值,将它们都初始化为0,并与线程关联起来。如图21-1每个线程都有自己的PVOID数组,数组中的每个PVOID可以保存任意值
为了将信息保存到线程的PVOID数组之前,需要知道哪个索引可以使用(调用
TlsAlloc获得))
为了把一个值放到线程的数组中,使用
TlsSetValue函数
WINBASEAPI
BOOL
WINAPI
TlsSetValue(
_In_ DWORD dwTlsIndex,
_In_opt_ LPVOID lpTlsValue
);
函数把dwTlsValue所标识的PVOID值放入线程的数组中,。dwTlsIndex是一个索引值,标识在数组中的具体位置。如果调用成功
TlsSetValue函数返回TRUE
一个线程调用
TlsSetValue的时候,会修改自己的数组。但其无法修改另一个线程的Tls值。如果要将一个线程的数据保存到另外一个线程上去。就是在创建线程的时候传一个值给他们。
在使用
TlsSetValue时应该总是传入前面调用
TlsAlloc时所返回的索引。
为了从线程数组取回一个值,使用
TlsGetValue
WINBASEAPI
LPVOID
WINAPI
TlsGetValue(
_In_ DWORD dwTlsIndex
);
函数会返回在索引为dwTlsIndex的TLS元素中保存的值。
当我们不在需要一个已经预定的Tls元素时,使用
TlsFree
WINBASEAPI
BOOL
WINAPI
TlsFree(
_In_ DWORD dwTlsIndex
);
如果DLL要使用TLS,在DllMain函数处理
DLL_PROCESS_ATTACH的时候调用
TlsAlloc,并在
DLL_PROCESS_DETACH的时候调用
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 (pSomeStruct != 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 value.
memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct));
}
else {
// the caller 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.
}
}
为了节省Tls的资源建议把Tls数据定义成结构体在Tls中保存结构体指针,这样新增数据仅仅修改结构体而不需要增加Tls索引的使用量。
再看以下代码。
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
// returned by the earlier call to TlsAlloc.
dwTlsIndex = TlsAlloc();
pvSomeValue = TlsGetValue(dwTlsIndex);
执行结果,最后pvSomeValue的值是0
TlsAlloc在返回之前会遍历进程中的每个线程,并根据新分配的索引,把每个线程数组中对应的元素设置为0.
21.2 静态TLS
__declspec(thread) DWORD gt_dwStartTime = 0;
__declspec(thread)所修饰的变量必须是全局或静态变量。
编译的时候,会把所有TLS变量放到一个叫.tls的段里。
为了让TLS能够正常工作,操作系统也必须参与进来。系统将程序载入到内存时,会查看可执行文件的.tls段。并分配一块足够大的内存来包含所有静态TLS变量。
编译器必须生成额外代码来引用TLS变量,使得应用程序更大,执行效率也慢。
如果DLL中存在.tls段,应用程序加载DLL的时候会计算自身和dll共同的.tls段的大小并相加以后分配一块足够大的内存来包含所有隐式链接的DLL需要的TLS变量。
如果DLL是显示连接的,也包含TLS变量。系统必须查看进程中的所有已有的线程,并扩大他们的TLS内存块。另外如果应用程序调用FreeLibrary来释放一个DLL,而该DLL包含了静态TLS变量。那么进程中每个线程相关的内存块也应该相应的缩减。