TLS---线程局部存储

2021/7/24 6:12:04

本文主要是介绍TLS---线程局部存储,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、介绍

概念:线程局部存储(Thread Local Storage,TLS)用来将数据与一个正在执行的指定线程关联起来。

  进程中的全局变量与函数内定义的静态(static)变量,是各个线程都可以访问的共享变量。在一个线程修改的内存内容,对所有线程都生效。这是一个优点也是一个缺点。说它是优点,线程的数据交换变得非常快捷。说它是缺点,一个线程死掉了,其它线程也性命不保; 多个线程访问共享数据,需要昂贵的同步开销,也容易造成同步相关的BUG。

  如果需要在一个线程内部的各个函数调用都能访问、但其它线程不能访问的变量(被称为static memory local to a thread 线程局部静态变量),就需要新的机制来实现。这就是TLS。

  线程局部存储在不同的平台有不同的实现,可移植性不太好。幸好要实现线程局部存储并不难,最简单的办法就是建立一个全局表,通过当前线程ID去查询相应的数据,因为各个线程的ID不同,查到的数据自然也不同了。大多数平台都提供了线程局部存储的方法,无需要我们自己去实现:

linux:

  int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));

  int pthread_key_delete(pthread_key_t key);

  void *pthread_getspecific(pthread_key_t key);

  int pthread_setspecific(pthread_key_t key, const void *value);

Win32

  方法一:每个线程创建时系统给它分配一个LPVOID指针的数组(叫做TLS数组),这个数组从C编程角度是隐藏着的不能直接访问,需要通过一些C API函数调用访问。首先定义一些DWORD线程全局变量或函数静态变量,准备作为各个线程访问自己的TLS数组的索引变量。一个线程使用TLS时,第一步在线程内调用TlsAlloc()函数,为一个TLS数组索引变量与这个线程的TLS数组的某个槽(slot)关联起来,例如获得一个索引变量:

  global_dwTLSindex=TLSAlloc();

  注意,此步之后,当前线程实际上访问的是这个TLS数组索引变量的线程内的拷贝版本。也就说,不同线程虽然看起来用的是同名的TLS数组索引变量,但实际上各个线程得到的可能是不同DWORD值。其意义在于,每个使用TLS的线程获得了一个DWORD类型的线程局部静态变量作为TLS数组的索引变量。C/C++原本没有直接定义线程局部静态变量的机制,所以在如此大费周折。

  第二步,为当前线程动态分配一块内存区域(使用LocalAlloc()函数调用),然后把指向这块内存区域的指针放入TLS数组相应的槽中(使用TlsValue()函数调用)。

  第三步,在当前线程的任何函数内,都可以通过TLS数组的索引变量,使用TlsGetValue()函数得到上一步的那块内存区域的指针,然后就可以进行内存区域的读写操作了。这就实现了在一个线程内部这个范围处处可访问的变量。

  最后,如果不再需要上述线程局部静态变量,要动态释放掉这块内存区域(使用LocalFree()函数),然后从TLS数组中放弃对应的槽(使用TlsFree()函数)。

功能:它主要是为了避免多个线程同时访存同一全局变量或者静态变量时所导致的冲突,尤其是多个线程同时需要修改这一变量时。为了解决这个问题,我们可以通过TLS机制,为每一个使用该全局变量的线程都提供一个变量值的副本,每一个线程均可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。而从全局变量的角度上来看,就好像一个全局变量被克隆成了多份副本,而每一份副本都可以被一个线程独立地改变。

  TLS 是一个良好的Win32 特质,让多线程程序设计更容易一些。TLS 是一个机制,经由它,程序可以拥有全域变量,但处于「每一线程各不相同」的状态。也就是说,进程中的所有线程都可以拥有全域变量,但这些变量其实是特定对某个线程才有意义。例如,你可能有一个多线程程序,每一个线程都对不同的文件写文件(也因此它们使用不同的文件handle)。这种情况下,把每一个线程所使用的文件handle 储存在TLS 中,将会十分方便。当线程需要知道所使用的handle,它可以从TLS 获得。重点在于:线程用来取得文件handle 的那一段码在任何情况下都是相同的,而从TLS中取出的文件handle 却各不相同。非常灵巧,不是吗?有全域变数的便利,却又分属各线程。  
   虽然TLS 很方便,它并不是毫无限制。在Windows NT 和Windows 95 之中,有64 个DWORD slots 供每一个线程使用。这意思是一个进程最多可以有64 个「对各线程有不同意义」的DWORDs。 虽然TLS 可以存放单一数值如文件handle,更常的用途是放置指针,指向线程的私有资料。有许多情况,多线程程序需要储存一堆数据,而它们又都是与各线程相关。许多程序员对此的作法是把这些变量包装为C 结构,然后把结构指针储存在TLS 中。当新的线程诞生,程序就配置一些内存给该结构使用,并且把指针储存在为线程保留下来的TLS 中。一旦线程结束,程序代码就释放所有配置来的区块。既然每一个线程都有64 个slots 用来储存线程自己的数据,那么这些空间到底打哪儿来?在线程的学习中我们可以从结构TDB中看到,每一个thread database 都有64 个DWORDs 给TLS 使用。当你以TLS 函式设定或取出数据,事实上你真正面对的就是那64 DWORDs。好,现在我们知道了原来那些“对各线程有不同意义的全局变量”是存放在线程各自的TDB中。 

  接下来你也许会问:我怎么存取这64个DWORDS呢?我又怎么知道哪个DWORDS被占用了,哪个没有被占用呢?首先我们要理解这样一个事实:系统之所以给我们提供TLS这一功能,就是为了方便的实现“对各线程有不同意义的全局变量”这一功能;既然要达到“全局变量”的效果,那么也就是说每个线程都要用到这个变量,既然这样那么我们就不需要对每个线程的那64个DWORDS的占用情况分别标记了,因为那64个DWORDS中的某一个一旦占用,是所有线程的那个DWORD都被占用了,于是KERNEL32 使用两个DWORDs(总共64 个位)来记录哪一个slot 是可用的、哪一个slot 已经被用。这两个DWORDs 可想象成为一个64 位数组,如果某个位设立,就表示它对应的TLS slot 已被使用。这64 位TLS slot 数组存放在process database 中(在进程一节中的PDB结构中我们列出了那两个DWORDs)。 

二、分类和用途

2.1分类

动态TLS和静态TLS。

2.2用途

  动态TLS和静态TLS这两项技术在创建DLL的时候更加有用,这是因为DLL通常并不知道它们被链接到的应用程序的结构是什么样的。

  • 如果应用程序高度依赖全局变量或静态变量,那么TLS可以成为我们的救生符。因而最好在开发中最大限度地减少对此类变量的使用,更多的依赖于自动变量(栈上的变量)和通过函数参数传入的数据,因为栈上的变量始终都是与某个特定的线程相关联的。如果不使用此类变量,那么就可以避免使用TLS。
  • 但是在编写应用程序时,我们一般都知道自己要创建多少线程,自己会如何使用这些线程,然后我们就可以设计一些替代方案来为每个线程关联数据,或者设计得好一点的话,可以使用基于栈的方法(局部变量)来为每个线程关联数据。

2.3 动态TLS

  运行在系统中的每一个进程都有如下图所示的一个位数组,每个标志可以被设为FREE或INUSE,表示该TLS元素是否正在被使用。

  进程中的线程是通过使用一个数组来保存与线程相关联的数据的,这个数组由TLS_MINIMUM_AVAILABLE个元素组成,在WINNT.H文件中该值被定义为64个。也就是说当线程创建时,系统给每一个线程分配了一个数组,这个数组共有TLS_MINIMUM_AVAILABLE个元素,并且将这个数组的各个元素初始化为0,之后系统把这个数组与新创建的线程关联起来。每一个线程中都有它自己的数组,数组中的每一个元素都能保存一个32位的值。在使用这个数组前首先要判定,数组中哪个元素可以使用,这将使用函数TlsAlloc来判断。函数TlsAlloc判断数组中一个元素可用后,就把这个元素分配给调用的线程,并保留给调用线程。要为数组中的某个元素赋值可以使用函数TlsSetValue,要得到某个元素的值可以使用TlsGetValue。

 一般通过调用一组4个API函数来使用动态TLS:TlsAlloc、TlsSetValue、TlsGetValue和TlsFree。

1)要使用动态TLS,必须先调用TlsAlloc函数:

1 DWORD WINAPI TlsAlloc(void);

  这个函数让系统对进程中的位标志进行检索并找到一个FREE标志,然后系统会将该标志从FREE改为INUSE并让TlsAlloc返回该标志在位数组中的索引。一个DLL(或应用程序)通常将这个索引保存在一个全局变量中。由于这个值会在整个进程地址范围内使用,而不是在线程范围内使用,因此这种情况下全局变量是一个更好的选择。

  如果TlsAlloc无法在列表中找到一个FREE标志,那么它会返回TLS_OUT_OF_INDEXES(在WinBase.h中被定义为0xFFFFFFFF)。

  当系统创建一个线程的时候,会分配TLS_MINIMUM_AVAILABLE个PVOID值,将它们都初始化为0,并与线程关联起来。每个线程都有自己的PVOID数组,数组中的每个PVOID可以保存任意值。在能够将信息保存到线程的PVOID数组中之前,我们必须知道数组中的哪个索引可供使用---这就是调用TlsAlloc的目的。TlsAlloc为我们预定了一个索引,如果为2,即TlsAlloc返回值为2,那么无论是进程中当前正在运行的线程,还是今后可能会创建的线程,都不能再使用该索引2了。如果不能找到一个值为FREE的成员,TlsAlloc函数就返回TLS_OUT_OF_INDEXES(在WinBase.h文件中定义为-1),意味着失败。

  例如,在第一次调用TlsAlloc的时候,系统发现位数组中第一个成员的值是FREE,它就将此成员的值改为INUSE,然后返回0。

  当一个线程被创建时,Windows就会在进程地址空间中为该线程分配一个长度为TLS_MINIMUM_AVAILABLE的数组,数组成员的值都被初始化为0。在内部,系统将此数组与该线程关联起来,保证只能在该线程中访问此数组中的数据。每个线程都有它自己的数组,数组成员可以存储任何数据。

2)为了把一个值放到线程的PVOID数组中,应该调用TlsSetValue函数:

1 BOOL WINAPI TlsSetValue(
2     __in      DWORD dwTlsIndex, //索引值,表示在数组中的具体位置
3     __in_opt  LPVOID lpTlsValue //要设置的值
4 );

  当一个线程调用TlsSetValue函数成功时,它会修改自己的PVOID数组,但它无法修改另一个线程的TLS值。在调用TlsSetValue时,我们应该总是传入前面在调用TlsAlloc时返回的索引。因为Windows为了效率牺牲了对输入值的错误检测。此函数调用成功,会返回TRUE。

3)为了从线程的数组中取回一个值,应该调用函数TlsGetValue:

1 LPVOID WINAPI TlsGetValue(
2       __in  DWORD dwTlsIndex //索引值
3 );

 这个函数会返回在索引为dwTlsIndex的TLS元素中保存的值。TlsGetValue只会查看属于调用线程的数组。

4)当不再需要一个已经预定的TLS元素时,应该调用TlsFree函数:

1 BOOL WINAPI TlsFree(
2       __in  DWORD dwTlsIndex //索引值
3 );

这个函数告诉系统已经预定的这个TLS元素现在不需要了,函数会将进程内的位标志数组中对应的INUSE标志重新设回FREE。此外,函数还会将所有线程中该元素的内容设为0.

一般情况下,为各线程分配TLS索引的工作要在主线程中完成,而分配的索引值应该保存在全局变量中,以方便各线程访问。示例1代码很清除地说明了这一点。主线程一开始就使用TlsAlloc为时间跟踪系统申请了一个索引,保存在全局变量g_tlsUsedTime中。之后,为了示例TLS机制的特点同时创建了10个线程。这10个线程最后都打印出了自己的生命周期,如图3.10所示。

使用动态TLS

  通常,如果DLL要使用TLS,那它会在DllMain函数处理DLL_PROCESS_ATTACH的时候调用TlsAlloc,在DllMain处理DLL_PROCESS_DETACH的时候调用TlsFree。而TlsSetValue和TlsGetValue的调用则最有可能发生在DLL所提供的其他函数中。而向应用程序中添加TLS的一种方法是直到需要时才添加。

示例1:

  利用TLS可以给特定的线程关联一个数据。比如下面的例子将每个线程的创建时间与该线程关联了起来,这样,在线程终止的时候就可以得到线程的生命周期。整个跟踪线程运行时间的例子的代码如下

 1 #include <stdio.h>
 2 #include <windows.h>
 3 #include <process.h>
 4 
 5 // 利用TLS记录线程的运行时间
 6 
 7 DWORD g_tlsUsedTime;
 8 void InitStartTime();
 9 DWORD GetUsedTime();
10 
11 
12 UINT __stdcall ThreadFunc(LPVOID)
13 {
14   int i;
15 
16   // 初始化开始时间
17   InitStartTime();
18 
19   // 模拟长时间工作
20   i = 10000*10000;
21   while(i--) { }
22 
23   // 打印出本线程运行的时间
24   printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d \n", 
25             ::GetCurrentThreadId(), GetUsedTime());
26   return 0;
27 }
28 
29 int main(int argc, char* argv[])
30 {
31   UINT uId;
32   int i;
33   HANDLE h[10];
34 
35   // 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统
36   g_tlsUsedTime = ::TlsAlloc(); 
37 
38   // 令十个线程同时运行,并等待它们各自的输出结果
39   for(i=0; i<10; i++)
40   {
41     h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
42   }
43   for(i=0; i<10; i++)
44   {
45     ::WaitForSingleObject(h[i], INFINITE);
46     ::CloseHandle(h[i]);
47   }
48 
49   // 通过释放线程局部存储索引,释放时间记录系统占用的资源
50   ::TlsFree(g_tlsUsedTime);
51   return 0;
52 }
53 
54 // 初始化线程的开始时间
55 void InitStartTime()
56 {
57   // 获得当前时间,将线程的创建时间与线程对象相关联
58   DWORD dwStart = ::GetTickCount();
59   ::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);
60 }
61 
62 // 取得一个线程已经运行的时间
63 DWORD GetUsedTime()
64 {
65   // 获得当前时间,返回当前时间和线程创建时间的差值
66   DWORD dwElapsed = ::GetTickCount();
67   dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);
68   return dwElapsed;
69 }

示例2:  

  线程运行时间记录系统仅提供InitStartTime和GetUsedTime两个函数供用户使用。应该在线程一开始就调用InitStartTime函数,此函数得到当前时间后,调用TlsSetValue将线程的创建时间保存在以g_tlsUsedTime为索引的线程数组中。当想查看线程的运行时间时,直接调用GetUsedTime函数就行了。这个函数使用TlsGetValue取得线程的创建时间,然后返回当前时间和创建时间的差值。

 1 /*!
 2  * 调用g_tlsUsedTime = TlsAlloc();时,获取一个未被占用的位,所有线程用的都是同一个位(g_tlsUsedTime),只不过在
 3  * 保存和获取value时,调用的是与各自线程关联的数组。那么问题来了,既然都是调用各自数组存取数据,为什么要用数组
 4  * 呢,难道是为了存取很多数据?可能是吧。
 5  *
 6  * 2015/07/24 by <felove>
 7  */
 8 #include <iostream>
 9 #include <stdio.h>
10 #include <windows.h>
11 #include <process.h>
12 
13 DWORD g_tlsUsedTime;
14 DWORD g_tlsUsedTime2;
15 void InitStartTime();
16 DWORD GetUsedTime();
17 DWORD GetThreadID();
18 
19 UINT __stdcall ThreadProc(LPVOID)
20 {
21     int i;
22     InitStartTime();
23     i = 10000*10000;
24     while(i--){}
25     printf("this thread coming to end.thread ID:%-5d, [%-5d] Used Time:%d\n", GetCurrentThreadId(), GetThreadID(), GetUsedTime());
26     return 0;
27 }
28 
29 int main()
30 {
31     UINT uid;
32     int i;
33     HANDLE h[10];
34 
35     //这里应该有获取不到可用位的判断。。
36     g_tlsUsedTime = TlsAlloc();
37     g_tlsUsedTime2 = TlsAlloc();
38 
39     for(i = 0; i < 10; ++i)
40     {
41         h[i] = (HANDLE)_beginthreadex(NULL, 0,ThreadProc, NULL, 0, &uid);
42     }
43 
44     for(i = 0; i < 10; ++i)
45     {
46         WaitForSingleObject(h[i],INFINITE);
47         CloseHandle(h[i]);
48     }
49 
50     TlsFree(g_tlsUsedTime);
51 
52     system("pause");
53     return 0;
54 }
55 
56 void InitStartTime()
57 {
58     DWORD dwStart = GetTickCount();
59     TlsSetValue(g_tlsUsedTime,(LPVOID)dwStart);
60     DWORD threadId = GetCurrentThreadId();
61     TlsSetValue(g_tlsUsedTime2,(LPVOID)threadId);
62 }
63 
64 DWORD GetUsedTime()
65 {
66     DWORD dwElapsed = GetTickCount();
67     dwElapsed = dwElapsed - (DWORD)TlsGetValue(g_tlsUsedTime);
68     return dwElapsed;
69 }
70 
71 DWORD GetThreadID()
72 {
73     DWORD threadId = (DWORD)TlsGetValue(g_tlsUsedTime2);
74     return threadId;
75 }

参考文章:

https://www.cnblogs.com/stli/archive/2010/11/03/1867852.html

https://www.cnblogs.com/felove2013/articles/4672297.html



这篇关于TLS---线程局部存储的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程