• 第21章 线程局部存储


    21.1 动态TLS

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

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

    21.1.2 动态TLS 

    (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】演示线程局部存储的例子

    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索引失败!
    "),
                             GetCurrentProcessId());
                    return FALSE;
                }
    
                _tprintf(_T("为进程[ID:0x%X]分配TLS索引:%u!
    "),
                         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]失败!
    "),
                             GetCurrentThreadId(), g_dwTLS,pThdData);
                    GRS_SAFEFREE(pThdData);
                    return FALSE;
                }
    
                _tprintf(_T("为线程[ID:0x%X]设置TLS索引[%u]变量[0x%08X]!
    "),
                         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]失败!
    "),
                             GetCurrentThreadId(), g_dwTLS, pThdData);
                    return FALSE;
                }
    
                _tprintf(_T("为线程[ID:0x%X]获取TLS索引[%u]变量[0x%08X]并销毁!
    "),
                         GetCurrentThreadId(), g_dwTLS, pThdData);
                GRS_SAFEFREE(pThdData);
            }
            break;
    
        case DLL_PROCESS_DETACH:
            {
                _tprintf(_T("释放进程[ID:0x%X]TLS索引[%u]
    "),
                     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],写入数据
    "),
                     GetCurrentThreadId(),pTlsBuf,nSize);
            StringCchPrintf(pTlsBuf, nSize / sizeof(TCHAR), _T("ID:0x%X"), GetCurrentThreadId());
            Sleep(1000);
            _tprintf(_T("线程[0x%X]取得缓冲区[地址:0x%08X 大小(字节):%u],写入的数据为[%s]
    "),
                     GetCurrentThreadId(), pTlsBuf, nSize,pTlsBuf);
        }
        _tprintf(_T("线程[0x%X]退出
    "),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;
    }
  • 相关阅读:
    golang 使用 protobuf 的教程
    golang语言中sync/atomic包的学习与使用
    《算法竞赛进阶指南》0x21有向无环图中点的可达性统计 topsort+bitset
    《算法竞赛进阶指南》0x21树和图的遍历 求dfs序以及树的重心
    《算法竞赛进阶指南》0x17二叉堆 利用优先队列求k叉哈夫曼树的最优结构
    《算法竞赛进阶指南》0x17二叉堆 链表+红黑树实现高效插入、删除、取最小值
    《算法竞赛进阶指南》0x17二叉堆 POJ2442 矩阵取数求前N大
    GIT-windows系统部署gitblit服务器
    mysql 端口修改
    VUE-开发工具VSCode
  • 原文地址:https://www.cnblogs.com/5iedu/p/5174760.html
Copyright © 2020-2023  润新知