21.1 动态TLS

21.1.1 为什么要使用线程局部存储

  编写多线程程序的时候都希望存储一些线程私有的数据,我们知道,属于每个线程私有的数据包括线程的栈和当前的寄存器,但是这两种存储都是非常不可靠的,栈会在每个函数退出和进入的时候被改变,而寄存器更是少得可怜。假设我们要在线程中使用一个全局变量,但希望这个全局变量是线程私有的,而不是所有线程共享的,该怎么办呢?这时候就须要用到线程局部存储(TLS,Thread Local Storage)这个机制了。

21.1.2 动态TLS第21章 线程局部存储_#define 

(1)每个进程都有一组正在使用标志,共TLS_MINIMUM_AVAILABLE个。每个标志可以被设为FREE或INUSE,表示该TLS元素是否正在使用。(注意这组标志属进程所有)

(2)当系统创建一个线程的时候,会为该线程分配与线程关联的、属于线程自己的PVOID型数组(共有TLS_MINIMUM_AVAILBALE个元素),数组中的每个PVOID可以保存任意值。

21.1.3 使用动态TLS

(1)调用TlsAlloc函数

  ①该函数会检索系统进程中的位标志并找到一个FREE标志,然后将该标志从FREE改为INUSE,并返回该标志在位数组中的索引,通常将该索引保存在一个全局变量中,因为这个值会在整个进程范围内(而不是线程范围内)使用。

  ②如果TlsAlloc无法在列表中找到一个FREE标志,会返回TLS_OUT_OF_INDEXES。

  ③以上就是TlsAlloc99%的工作,剩1%的工作就是在函数返回之前,会遍历进程中的每个线程,并根据新分配的索引,在每个线程的Tls数组中把对应的素素设为0(具体原因请看后面的分析)。

(2)调用TlsSetValue(dwTlsIndex,pvTlsValue)将一个值放到线程的数组中

  ①该函数把pvTlsValue所标志的一个PVOID值放到线程的数组中,dwTlsIndex指定了在数组中的具体位置(由TlsAlloc得到)

  ②当一个线程调用TlsSetValue的时候,会修改自己的数组。但它无法修改另一个线程的TLS数组中的值。

(3)从线程的数组中取回一个值:PVOID TlsGetValue(dwTlsIndex)

  ①与TlsSetValue相似,TlsGetValue中会查看属于调用线程的数组

(4)释放己经预订的TLS元素:TlsFree(dwTlsIndex)

  ①该函数会将进程内的位标志数组对应的INUSE标志重设回FREE

  ②同时该函数还会将所有线程中该元素的内容设为0。

  ③试图释放一个尚未分配的TLS元素将导致错误

21.1.4编写类似_tcstok_s函数

DWORD g_dwTlsIndex; //假设这个全局变量是通过TlsAlloc函数来初始化的

void MyFunction(PSOMESTRUCT PSomeStruct){

    if (pSomeStruct != NULL){
        //调用者正在启用该函数,就像strok函数第一次传入非NULL,
        //以后传为NULL

        //检查是否己经为数据分配存储空间
        if (TlsGetValue(g_dwTlsIndex) == NULL)
            //线程第一次调用该函数时,该空间尚未分配
            //TlsAlloc函数返回之前,会将进程中所有线程的g_dwTlsIndex元素
            //清零,以保证这句代码不会出错非空的现象!

            //通过TLS能保证分配的空间只与调用线程相关联
            TlsSetValue(g_dwTlsIndex,  
                        HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct));
        }

        //将传入的pSomeStruct数据保存刚才那个只与调用线程相关的存储空间中
        memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct));
    } else{ 
        //调用者己经第二次(或以上)调用该函数,会传入NULL参数
 
        //取出数据
        pSomeStruct = (PSOMESTRUCT)TlsGetValue(g_dwTlsIndex);

        //以下可以开始pSomeStruct这个数据了。
        ...
    }
}

21.2 静态TLS

(1)静态TLS变量的声明

  ①__thread int number;//GCC使用__thread关键字声明

  ②__declspec(thread) int number; //MSVC使用__declspec(thread)声明

(2)Windows中静态TLS的实现原理

  ①对于Windows系统来说,正常情况下一个全局变量或静态变量会被放到".data"或".bss"段中,但当我们使用__declspec(thread)定义一个线程私有变量的时候,编译器会把这些变量放到PE文件的".tls"段中。

  ②当系统启动一个新的线程时,它会从进程的堆中分配一块足够大小的空间,然后把".tls"段中的内容复制到这块空间中,于是每个线程都有自己独立的一个".tls"副本。所以对于用__declspec(thread)定义的同一个变量,它们在不同线程中的地址都是不一样的。

  ③对于一个TLS变量来说,它有可能是一个C++的全局对象,那么每个线程在启动时不仅仅是复制".tls"的内容那么简单,还需要把这些TLS对象初始化,必须逐个地调用它们的全局构造函数,而且当线程退出时,还要逐个地将它们析构,正如普通的全局对象在进程启动和退出时都要构造、析构一样。

  ④Windows PE文件的结构中有个叫数据目录的结构。它总共有16个元素,其中有一元素下标为IMAGE_DIRECT_ENTRY_TLS,这个元素中保存的地址和长度就是TLS表(IMAGE_TLS_DIRECTORY结构)的地址和长度。TLS表中保存了所有TLS变量的构造函数和析构函数的地址,Windows系统就是根据TLS表中的内容,在每次线程启动或退出时对TLS变量进行构造和析构。TLS表本身往往位于PE文件的".rdata"段中。

  ⑤另外一个问题是,既然同一个TLS变量对于每个线程来说它们的地址都不一样,那么线程是如何访问这些变量的呢?其实对于每个Windows线程来说,系统都会建立一个关于线程信息的结构,叫做线程环境块(TEB,Thread Environment Block)。这个结构里面保存的是线程的堆栈地址、线程ID等相关信息,其中有一个域是一个TLS数组,它在TEB中的偏移是0x2C。对于每个线程来说,x86的FS段寄存器所指的段就是该线程的TEB,于是要得到一个线程的TLS数组的地址就可以通过FS:[0x2C]访问到。

  ⑥这个TLS数组对于每个线程来说大小是固定的,一般有64个元素。而TLS数组的第一个元素就是指向该线程的".tls"副本的地址。于是要得到一个TLS的变量地址的步骤为:首先通过FS:[0x2C]得到TLS数组的地址,然后根据TLS数组的地址得到".tls"副本的地址,然后加上变量在".tls"段中的偏移即该TLS变量在线程中的地址。

【DllTls】演示线程局部存储的例子

第21章 线程局部存储_#define_02

1、动态链接库端的代码:

/************************************************************************
Module: DllTls.h
************************************************************************/
#pragma  once
#include <windows.h>

#ifdef DLLTLS_EXPORT
//DLLTLS_EXPORT必须在Dll源文件包含该头件前被定义
#define DLLTLSAPI extern "C" __declspec(dllexport)
//本例中所有的函数和变量都会被导出
#else
#define DLLTLSAPI extern "C" __declspec(dllimport)
#endif

//定义要导出的函数的原型
DLLTLSAPI VOID* GetThreadBuf();
DLLTLSAPI UINT GetThreadBufSize();
#include <tchar.h>
#include <windows.h>
#include <strsafe.h>
#include <locale.h>

//在这个DLL源文件定义要导出的函数和变量
#define DLLTLS_EXPORT   //这个源文件中须定义这个宏,以告诉编译器函数要
//__declspect(dllexport),这个宏须在包含MyLib.h
//之前被定义

#include "DllTls.h"

#define GRS_ALLOC(sz)    HeapAlloc(GetProcessHeap(),0,sz)
#define GRS_CALLOC(sz)   HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,sz)
#define GRS_SAFEFREE(p)  if(NULL!=p){HeapFree(GetProcessHeap(),0,p);p=NULL;}

static DWORD g_dwTLS = 0;
static UINT g_nBufSize = 256;

BOOL APIENTRY DllMain(HANDLE hDllHandle, DWORD dwReason, LPVOID lpreserved){
    switch (dwReason)
    {
    case DLL_PROCESS_ATTACH:
        {
            _tsetlocale(LC_ALL, _T("chs"));
            g_dwTLS = TlsAlloc();
            if (TLS_OUT_OF_INDEXES == g_dwTLS){
                _tprintf(_T("为进程[ID:0x%X]分配TLS索引失败!\n"),
                         GetCurrentProcessId());
                return FALSE;
            }

            _tprintf(_T("为进程[ID:0x%X]分配TLS索引:%u!\n"),
                     GetCurrentProcessId(),g_dwTLS);
        }
        break;

    case DLL_THREAD_ATTACH:
        {
            PVOID pThdData = GRS_CALLOC(g_nBufSize);

            if (!TlsSetValue(g_dwTLS,pThdData)){
                _tprintf(_T("为线程[ID:0x%X]设置TLS索引[%u]变量[0x%08X]失败!\n"),
                         GetCurrentThreadId(), g_dwTLS,pThdData);
                GRS_SAFEFREE(pThdData);
                return FALSE;
            }

            _tprintf(_T("为线程[ID:0x%X]设置TLS索引[%u]变量[0x%08X]!\n"),
                     GetCurrentThreadId(), g_dwTLS, pThdData);
        }
        break;

    case DLL_THREAD_DETACH:
        {
            PVOID pThdData = TlsGetValue(g_dwTLS);

            if (!pThdData){
                _tprintf(_T("为线程[ID:0x%X]获取TLS索引[%u]变量[0x%08X]失败!\n"),
                         GetCurrentThreadId(), g_dwTLS, pThdData);
                return FALSE;
            }

            _tprintf(_T("为线程[ID:0x%X]获取TLS索引[%u]变量[0x%08X]并销毁!\n"),
                     GetCurrentThreadId(), g_dwTLS, pThdData);
            GRS_SAFEFREE(pThdData);
        }
        break;

    case DLL_PROCESS_DETACH:
        {
            _tprintf(_T("释放进程[ID:0x%X]TLS索引[%u]\n"),
                 GetCurrentProcessId(), g_dwTLS);
            TlsFree(g_dwTLS);
        }
        break;
    }

    return TRUE;
}

VOID* GetThreadBuf()
{
    return TlsGetValue(g_dwTLS);
}

UINT GetThreadBufSize(){
    return g_nBufSize;
}

2、测试程序

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include <locale.h>
#include "../../Chap21/21_DLLTls/DllTls.h"

#pragma comment(lib,"../../Debug/21_DLLTls.lib")

#define GRS_CREATETHREAD(Fun,Param) CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)Fun,Param,0,NULL)

DWORD WINAPI ThreadProc(LPVOID pvParam){
    TCHAR* pTlsBuf = (TCHAR*)GetThreadBuf();
    UINT nSize = GetThreadBufSize();
    if (NULL !=pTlsBuf && 0 != nSize){
        _tprintf(_T("线程[0x%X]取得缓冲区[地址:0x%08X 大小(字节):%u],写入数据\n"),
                 GetCurrentThreadId(),pTlsBuf,nSize);
        StringCchPrintf(pTlsBuf, nSize / sizeof(TCHAR), _T("ID:0x%X"), GetCurrentThreadId());
        Sleep(1000);
        _tprintf(_T("线程[0x%X]取得缓冲区[地址:0x%08X 大小(字节):%u],写入的数据为[%s]\n"),
                 GetCurrentThreadId(), pTlsBuf, nSize,pTlsBuf);
    }
    _tprintf(_T("线程[0x%X]退出\n"),GetCurrentThreadId());
    return 0;
}

#define THREADCNT  2
int _tmain(){

    _tsetlocale(LC_ALL, _T("chs"));

    HANDLE phThread[THREADCNT] = {};
    for (int i = 0; i < THREADCNT; i++){
        phThread[i] = GRS_CREATETHREAD(ThreadProc, NULL);
    }

    WaitForMultipleObjects(THREADCNT, phThread, TRUE, INFINITE);
    for (int i = 0; i < THREADCNT;i++){
        CloseHandle(phThread[i]);
    }
    _tsystem(_T("PAUSE"));
    return 0;
}