作者:刘树伟
日期:2021年3月16日 14:09
参考:《Windows核心编程》第5版第21章
线程局部存储区,简称TLS,目的是给线程附加一个自定义数据。有两种方法为线程指定自定义数据,方法一是动态TLS,方法二是静态TLS。
动态TLS:
你可以理解为系统中为每个进程定义了一个bool类型,长度为64的数组:
bool g_TlsFlag[64];
(数组的长度可根据需要,动态扩展到1000多位。)数组的元素记录了是否被使用。
系统在创建每个线程的时候,也会分配一个void *类型、长度为64的数组:
void *TlsDataArray[64];
(数组的长度可根据需要,动态扩展到1000多位。)并把这个数组绑定到线程上。
所以,一个进程中,只有一个g_TlsFlag数组,和与线程数量相对应多的TlsDataArray数组。按最容易想的思路,当我们想为某个线程指定自定义数据的时候,只要遍历线程自己的TlsDataArray数组,找到个空位,把自定义数据设进去就行,根本不需要g_TlsFlag。但是windows是不这样做的,它的做法是通过调用TlsAlloc函数从进程的g_TlsFlag中找到个空位,然后把已运行及将来运行的所有线程绑定的TlsDataArray中相应的空位霸占上(即使其中的某些线程不需要设置自定义数据),并返回它的索引。这样,我们就可以通过这个索引,调用TlsSetValue为调用者线程的TlsDataArray元素赋自定义值了。这个返回的索引,一般保存成全局变量。
通过上面的描述,可以看到,通过TlsAlloc返回的索引,更像是类型索引。比如第一次调用TlsAlloc返回3,用来保存每个线程创建时间,那么所有线程的TlsDataArray的索引为3的元素,都用来保存线程创建时间。第二次调用TlsAlloc返回4,那么所有线程的TlsDataArray的索引为4的元素,可能都用来保存进程ID。所有索引为5的元素,可能都用来保存主窗口句柄,以此类推。
在实际开发中,为了节约索引位,我们一般只霸占一个索引位,然后定义一个结构体,把需要保存的自定义数据放到结构体中,然后把结构体指针保存到TLS中。如果有新的自定义数据需要保存,那么扩展结构体即可。
MFC中的线程状态信息,就是通过TLS保存的。MFC把自己用到的信息,放到结构体CThreadData中,占用的Tls槽索引保存在全局变量_afxThreadData->m_tlsIndex中。CThreadData中又有一个pData数据,每个元素,保存不同的数据,其中索引为_afxThreadState::m_nSlot的pData元素,保存的就是_AFX_THREAD_STATE *。
更多信息,请参考<Document/MFC>
示例代码:
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <atlstr.h>
// 自定义数据霸占的空闲 “进程Tls标志位数组” 索引。
DWORD g_dwTlsThreadIndex = -1;
void TlsThread(void *pArg)
{
int nThreadIndex = (int)pArg;
// 为 “本线程Tls数组” 的索引为g_dwTlsThreadIndex的元素设置自定义数据nThreadIndex
TlsSetValue(g_dwTlsThreadIndex, (void *)nThreadIndex);
while(1)
{
Sleep(rand() % 500);
// 获取本线程保存的自定义数据。
int nTlsThreadIndex = (int)TlsGetValue(g_dwTlsThreadIndex);
CString strInfo;
strInfo.Format(_T("Thread index:%d,\tTls thread index: %d.\r\n"), nThreadIndex, nTlsThreadIndex);
_tprintf(_T("%s"), (LPCTSTR)strInfo);
// 确保保存的数据与获取的数据相同。
_ASSERT(nThreadIndex == nTlsThreadIndex);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
// 提前霸占 “进程Tls标志位数组” 空闲槽,并把空闲槽索引保存到全局变量g_dwTlsThreadIndex中。
g_dwTlsThreadIndex = TlsAlloc();
// 接下来创建的每个线程,“线程Tls数组” 的g_dwTlsThreadIndex位元素,都被提前预定,供我们保存自定义数据。
for (int i = 0; i < 100; ++i)
{
HANDLE hThread = (HANDLE)_beginthread(TlsThread, 0, (void *)i);
CloseHandle(hThread);
hThread = NULL;
}
Sleep(1000 * 6000);
TlsFree(g_dwTlsThreadIndex);
return 0;
}
静态TLS:
相比动态TLS,静态TLS使用要简单的多。你只需要像下面这样定义一个全局变量,它就可以变成线程拥有了:
__declspec(thread) int gt_nTls = 5;
表面上看gt_nTls是一个全局变量,但由于加上了__declspec(thread)修饰符,它就变成线程拥有了,每个线程都可以访问gt_nTls,但每个线程访问的是自己的gt_nTls,在一个线程中修改了gt_nTls的值,也不会影响其它线程的gt_nTls。并且修改和读取gt_nTls不需要做数据保护。代码如下:
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <atlstr.h>
// 静态TLS变量
__declspec(thread) int gt_nTls = 5;
// 每个线程把gt_nTls的值设置为线程索引,你会发现,每个线程的gt_nTls值是不同的。
void TlsThread(void *pArg)
{
int nThreadIndex = (int)pArg;
gt_nTls = nThreadIndex;
while(1)
{
Sleep(rand() % 500);
CString strInfo;
strInfo.Format(_T("Thread index:%d,\tgt_nTls value: %d.\r\n"), nThreadIndex, gt_nTls);
_tprintf(_T("%s"), (LPCTSTR)strInfo);
_ASSERT(nThreadIndex == gt_nTls);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
// 创建100个线程。
for (int i = 0; i < 100; ++i)
{
HANDLE hThread = (HANDLE)_beginthread(TlsThread, 0, (void *)i);
CloseHandle(hThread);
hThread = NULL;
}
Sleep(1000 * 6000);
return 0;
}
本文介绍了线程局部存储(TLS)的概念,包括动态TLS和静态TLS两种实现方式。动态TLS通过系统分配索引,允许各线程存储自定义数据;静态TLS通过__declspec(thread)关键字实现,每个线程拥有独立的变量副本。

1424

被折叠的 条评论
为什么被折叠?



