Windows 线程局部存储区TLS

本文介绍了线程局部存储(TLS)的概念,包括动态TLS和静态TLS两种实现方式。动态TLS通过系统分配索引,允许各线程存储自定义数据;静态TLS通过__declspec(thread)关键字实现,每个线程拥有独立的变量副本。

作者:刘树伟
日期: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;
}
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值