刘收获

导航

< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

线程局部存储TLS

1 .使用线程局部存储的理由
   当我们希望这个进程的全局变量变为线程私有时,而不是所有线程共享的,也就是每个线程拥有一份副本时,这时候就可以用到线程局部存储(TLS,Thread Local Storage)这个机制了。

 

 

2.动态TLS
(1)调用TlsAlloc函数
    两种方式:

         1>全局调用一次:   global_dwTLSindex=TLSAlloc(); 

              如果程序只调用一次TlsAlloc,而不是每个线程都调用一次TlsAlloc()函数的话,多个线程就使用同一个索引值,不同线程虽然看起来用的是同名的TLS数组索引变量,但实际上各个线程得到的可能是                  不同DWORD值。其意义在于,每个使用TLS的线程获得了一个DWORD类型的线程局部静态变量作为TLS数组的索引变量。 

        2>进程内的每个线程都调用一次

   每个线程得到不一样的索引值,调试可以看到每一次调用,索引值的大小是自加1而递增的。

 

       函数流程:

  1>该函数会检索系统进程中的位标志并找到一个FREE标志,然后将该标志从FREE改为INUSE,并返回该标志在位数组中的索引,通常将该索引保存在一个全局变量中,因为这个值会在整个进程范围内(而不是线程范围内)使用。
  2>如果TlsAlloc无法在列表中找到一个FREE标志,会返回TLS_OUT_OF_INDEXES。
  3>TlsAlloc函数在函数返回之前,会遍历进程中的每个线程,并根据新分配的索引,在每个线程的Tls数组中把对应的元素设为0

             
(2)调用TlsSetValue(dwTlsIndex,pvTlsValue)

         将一个值放到线程的数组中
   该函数把pvTlsValue所标志的一个PVOID值放到线程的数组中,dwTlsIndex指定了在数组中的具体位置(由TlsAlloc得到)
   当一个线程调用TlsSetValue的时候,会修改自己的数组。但它无法修改另一个线程的TLS数组中的值。
(3)调用PVOID TlsGetValue(dwTlsIndex)

         从线程的数组中取回一个值
    与TlsSetValue相似,TlsGetValue中会查看属于调用线程的数组
(4)调用TlsFree(dwTlsIndex)

   释放己经预订的TLS元素
   该函数会将进程内的位标志数组对应的INUSE标志重设回FREE
   同时该函数还会将所有线程中该元素的内容设为0。
   试图释放一个尚未分配的TLS元素将导致错误

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// DynamicTLS.cpp : 定义控制台应用程序的入口点。
//
 
#include "stdafx.h"
#include <windows.h>
#include <process.h>
 
#include <ctime>
#include <cstdlib>
#include <vector>
#include <iostream>
 
using namespace std;
 
clock_t gc_begin = 0;
clock_t gc_End = 0;
clock_t gc_Interval = 0;   //全局
 
//unsigned long __stdcall threadProc(void *arg);       CreateThread
unsigned int __stdcall ThreadProc(void *arg);
 
int main(int argc, char *argv[])
{
    /*
    不同线程虽然看起来用的是同名的TLS数组索引变量,但实际上各个线程得到的可能是不同DWORD值。
    其意义在于,每个使用TLS的线程获得了一个DWORD类型的线程局部静态变量作为TLS数组的索引变量。
    */
 
    //这里使用 TlsAlloca() 可以,在线程内部调用这个函数也可以,使得每个线程都有一个不同的索引值,
    //仅仅在这里调用一次,程序的实现也成功了,不同线程使用了同一个索引值也成功了
 
    //DWORD tlsIndex = TlsAlloc();    //此步之后,当前线程实际上访问的是这个TLS数组索引变量的线程内的拷贝版本
    //这里调用 TlsAlloc()产生的第一个索引值也是1
    DWORD tlsIndex  = 0;
    vector<HANDLE> threads;
    for (int i = 0; i < 2; ++i) {
        HANDLE h = (HANDLE)_beginthreadex(NULL, 0, ThreadProc,
            (void*)tlsIndex, 0, NULL);
        /*
        使用_beginthreadex 和 CreateThread 创建线程产生的第一个索引值TlsAlloc()不同,
        CreateThread是 1,_beginthreadex产生的是3,占用了两个索引位?
        */
        //HANDLE h = (HANDLE)CreateThread(NULL, 0, ThreadProc,
        //(void*)tlsIndex, 0, NULL);
        Sleep(2000);
        threads.push_back(h);
    }
 
    for (size_t i = 0; i < threads.size(); ++i) {
        WaitForSingleObject(threads[i], INFINITE);
        CloseHandle(threads[i]);
    }
}
//
 
unsigned int __stdcall ThreadProc(void *arg)
{
    DWORD tlsIndex = TlsAlloc();
    //DWORD tlsIndex = reinterpret_cast<DWORD>(arg);
    gc_begin = clock();              //从“开启这个程序进程”到“程序中调用clock()函数”时之间的CPU时钟计时单元(clock tick)数
    TlsSetValue(tlsIndex, PVOID(gc_begin));  // 利用TlsSetValue 设置 值
 
    printf("Thread ID: %d, Thread Begin Time: %d\n", GetCurrentThreadId(), gc_begin);
 
    Sleep(2000);
 
    gc_End = clock();
    gc_Interval = gc_End - reinterpret_cast<clock_t>(TlsGetValue(tlsIndex));
    double sec = 1.0 * gc_Interval / CLOCKS_PER_SEC; // 利用TlsGetValue取得值
 
    printf("Thread ID: %d, Thread End Time: %d, Survival Time: %f\n", GetCurrentThreadId(), gc_End, sec);
 
    return 0;
}

  

   这里补充一下_beginthreadex()函数和CreateThread函数在创建线程时候,所创建的线程,调用TlsAlloc函数产生的索引值的不同,_beginthreadex()函数下的线程第一个索引值是3,而CreateThread函数下的线程第一个索引值是1,应该是_beginthreadex()函数下的线程出于某种原因占用了两位:

 

CreateThread函数下的线程第一个索引值:

 

 

_beginthreadex()函数下的线程第一个索引值:

 

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


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

  线程环境块(TEB,Thread Environment Block)。这个结构里面保存的是线程的堆栈地址、线程ID等相关信息,其中有一个域是一个TLS数组,它在TEB中的偏移是0x2C。对于每个线程来说,x86的FS段寄存器所指的段就是该线程的TEB,于是要得到一个线程的TLS数组的地址就可以通过FS:[0x2C]访问到。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Static_TLS.cpp : 定义控制台应用程序的入口点。
//
 
#include "stdafx.h"
#include <windows.h>
#include <iostream>
 
 
// 定义静态TLS全局变量
__declspec(thread) int __TlsValue = 0;
using namespace std;
DWORD WINAPI ThreadProcedure(LPVOID ParameterData);
int main()
{
 
    // 设置主线程静态TLS的value为5
    __TlsValue = 5;
    HANDLE ThreadHandle = CreateThread(NULL, 0, ThreadProcedure, NULL, 0, NULL);
    if (ThreadHandle)
    {
        // 等待直到子线程结束
        WaitForSingleObject(ThreadHandle, INFINITE);
        // 取得主线程静态TLS的值
        cout << "主线程 __TlsValue=" << __TlsValue << endl;
        }
    return 0;
}
DWORD WINAPI ThreadProcedure(LPVOID ParameterData)
{
    // 设置子线程value为10,并不影响主线程
    __TlsValue = 10;
    // 取得子线程静态TLS的值
    cout << "子线程 __TlsValue=" << __TlsValue << endl;
    return 0;
}

  

 

posted on   沉疴  阅读(724)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示