《Windows核心编程》读书笔记二十一章 线程局部存储区

来源:互联网 发布:递延所得税资产 知乎 编辑:程序博客网 时间:2024/06/05 16:30

第二十一章 线程局部存储区


本章内容

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)每个标志可以设为FREEINUSE,表示该TLS元素是否正在使用。MS保证至少有TLS_MINIMUM_AVAILABLE个标志可供使用。TLS_MINIMUM_AVAILABLE在Winnt.h中被定义为64,实际上最多可达1000多个。

首先需要使用TlsAlloc()
_Must_inspect_result_WINBASEAPIDWORDWINAPITlsAlloc(    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函数
WINBASEAPIBOOLWINAPITlsSetValue(    _In_ DWORD dwTlsIndex,    _In_opt_ LPVOID lpTlsValue    );



函数把dwTlsValue所标识的PVOID值放入线程的数组中,。dwTlsIndex是一个索引值,标识在数组中的具体位置。如果调用成功TlsSetValue函数返回TRUE

一个线程调用TlsSetValue的时候,会修改自己的数组。但其无法修改另一个线程的Tls值。如果要将一个线程的数据保存到另外一个线程上去。就是在创建线程的时候传一个值给他们。

在使用TlsSetValue时应该总是传入前面调用TlsAlloc时所返回的索引。
为了从线程数组取回一个值,使用TlsGetValue
WINBASEAPILPVOIDWINAPITlsGetValue(    _In_ DWORD dwTlsIndex    );
函数会返回在索引为dwTlsIndex的TLS元素中保存的值。

当我们不在需要一个已经预定的Tls元素时,使用TlsFree
WINBASEAPIBOOLWINAPITlsFree(    _In_ DWORD dwTlsIndex    );


使用动态TLS

如果DLL要使用TLS,在DllMain函数处理DLL_PROCESS_ATTACH的时候调用TlsAlloc,并在DLL_PROCESS_DETACH的时候调用TlsFree
TlsSetValueTlsGetValue的调用最可能发生在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变量。那么进程中每个线程相关的内存块也应该相应的缩减。
阅读全文
0 0