统计

  • 随笔 - 50
  • 文章 - 42
  • 评论 - 147
  • 引用 - 0

留言簿(6)

随笔分类

文章分类

Link

搜索

  •  

积分与排名

  • 积分 - 160257
  • 排名 - 161

最新评论

阅读排行榜

评论排行榜

使用线程局部存储TLS

Thread local storage (TLS)统一进程的多个线程可以通过由TlsAlloc方法返回的索引值在线程自身的空间内存储和取回一个值。在以下这个例子里,索引值在进程开始时创建,当各个线程启动时,会各自申请一块动态内存并且将内存指针通过TlsSetValue方法存储到各自的TLS空间中(由先前的索引值标定)。CommonFunc方法使用TlsGetValue方法通过索引取得数据指针。在各个线程结束前,释放动态内存块。在进程结束见,调用TlsFree方法释放索引。

 1#include <windows.h> 
 2#include <stdio.h> 
 3 
 4#define THREADCOUNT 4 
 5DWORD dwTlsIndex; 
 6 
 7VOID ErrorExit(LPSTR); 
 8 
 9VOID CommonFunc(VOID) 
10
11   LPVOID lpvData; 
12 
13// Retrieve a data pointer for the current thread. 
14 
15   lpvData = TlsGetValue(dwTlsIndex); 
16   if ((lpvData == 0&& (GetLastError() != ERROR_SUCCESS)) 
17      ErrorExit("TlsGetValue error"); 
18 
19// Use the data stored for the current thread. 
20 
21   printf("common: thread %d: lpvData=%lx\n"
22      GetCurrentThreadId(), lpvData); 
23 
24   Sleep(5000); 
25}
 
26 
27DWORD WINAPI ThreadFunc(VOID) 
28
29   LPVOID lpvData; 
30 
31// Initialize the TLS index for this thread. 
32 
33   lpvData = (LPVOID) LocalAlloc(LPTR, 256); 
34   if (! TlsSetValue(dwTlsIndex, lpvData)) 
35      ErrorExit("TlsSetValue error"); 
36 
37   printf("thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData); 
38 
39   CommonFunc(); 
40 
41// Release the dynamic memory before the thread returns. 
42 
43   lpvData = TlsGetValue(dwTlsIndex); 
44   if (lpvData != 0
45      LocalFree((HLOCAL) lpvData); 
46 
47   return 0
48}
 
49 
50int main(VOID) 
51
52   DWORD IDThread; 
53   HANDLE hThread[THREADCOUNT]; 
54   int i; 
55 
56// Allocate a TLS index. 
57 
58   if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES) 
59      ErrorExit("TlsAlloc failed"); 
60 
61// Create multiple threads. 
62 
63   for (i = 0; i < THREADCOUNT; i++
64   
65      hThread[i] = CreateThread(NULL, // default security attributes 
66         0,                           // use default stack size 
67         (LPTHREAD_START_ROUTINE) ThreadFunc, // thread function 
68         NULL,                    // no thread function argument 
69         0,                       // use default creation flags 
70         &IDThread);              // returns thread identifier 
71 
72   // Check the return value for success. 
73      if (hThread[i] == NULL) 
74         ErrorExit("CreateThread error\n"); 
75   }
 
76 
77   for (i = 0; i < THREADCOUNT; i++
78      WaitForSingleObject(hThread[i], INFINITE); 
79 
80   TlsFree(dwTlsIndex);
81
82   return 0
83}
 
84 
85VOID ErrorExit (LPSTR lpszMessage) 
86
87   fprintf(stderr, "%s\n", lpszMessage); 
88   ExitProcess(0); 
89}

90

常用情景:
各个线程所处理的对象有所不同,但是所需要的处理却可能类似,例如多个线程同时处理多个文件,就可以将文件句柄存在在相应的Tls中,在使用相同的接口进行处理
背景知识:
每个线程除了共享进程的资源外还拥有各自的私有资源:一个寄存器组(或者说是线程上下文);一个专属的堆栈;一个专属的消息队列;一个专属的Thread Local Storage(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)。
 
下面的四个函数就是对TLS进行操作的:

  (1)TlsAlloc  

上面我们说过了KERNEL32 使用两个DWORDs(总共64 个位)来记录哪一个slot 是可用的、哪一个slot 已经被用。当你需要使用一个TLS slot 的时候,你就可以用这个函数将相应的TLS slot位置1。 

 (2)TlsSetValue  

TlsSetValue 可以把数据放入先前配置到的TLS slot 中。两个参数分别是TLS slot 索引值以及欲写入的数据内容。TlsSetValue 就把你指定的数据放入64 DWORDs 所组成的数组(位于目前的thread database)的适当位置中。  

 (3)TlsGetValue  

这个函数几乎是TlsSetValue 的一面镜子,最大的差异是它取出数据而非设定数据。和TlsSetValue 一样,这个函数也是先检查TLS 索引值合法与否。如果是,TlsGetValue 就使用这个索引值找到64 DWORDs 数组(位于thread database 中)的对应数据项,并将其内容传回。  

 (4)TlsFree  

这个函数将TlsAlloc 和TlsSetValue 的努力全部抹消掉。TlsFree 先检验你交给它的索引值是否的确被配置过。如果是,它将对应的64 位TLS slots 位关闭。然后,为了避免那个已经不再合法的内容被使用,TlsFree 巡访进程中的每一个线程,把0 放到刚刚被释放的那个TLS slot 上头。于是呢,如果有某个TLS 索引后来又被重新配置,所有用到该索引的线程就保证会取回一个0 值,除非它们再调用TlsSetValue。

posted on 2008-12-10 21:25 pear_li 阅读(2050) 评论(4)  编辑 收藏 引用 所属分类: C++

评论

# re: 使用线程局部存储TLS  2008-12-11 09:47 LOGOS

终究是线程 “局部” 存储,看不出和传参给线程相比,优势在哪里
  回复  更多评论    

# re: 使用线程局部存储TLS  2008-12-11 16:36 阿福

我觉得线程局部存储的优势不大,除非和编译器结合起来。
比如,在线程函数的栈上定义一个对象,在线程函数退出的时候,对象自动调用自己的析构函数清理空间,同样能够达到线程局部存储的效果。

所谓线程局部存储与编译器结合,是指这样的效果:
TLS int errno;
//假设存在关键字TLS,这样在全局声明的变量,就会被编译器自动编译为线程局部存储的变量。可惜都没编译器支持这样的操作,相信最新的支持并行的语言会有类似的功能。
  回复  更多评论    

# re: 使用线程局部存储TLS  2008-12-12 09:45 guest

@LOGOS

当线程不是自己创建那么就很有用了。

假设A线程会依次执行fun1,fun2,fun3函数(或者回调吧),fun1要传递一些似有数据给fun3怎么办?使用全局变量的时候要处理同步问题,用TLS就不存在这个问题了。最经常见到的应用是标准库里面的strtok等。

------------------------------
C / C + +运行期库要使用线程本地存储器(T L S)。由于运行期库是在多线程应用程序出现前
的许多年设计的,因此运行期库中的大多数函数是用于单线程应用程序的。函数 s t r t o k就是个
很好的例子。应用程序初次调用 s t r t o k时,该函数传递一个字符串的地址,并将字符串的地址
保存在它自己的静态变量中。当你将来调用s t r t o k函数并传递N U L L时,该函数就引用保存的字
符串地址。
在多线程环境中,一个线程可以调用 s t r t o k,然后,在它能够再次调用该函数之前,另一
个线程也可以调用 S t r t o k。在这种情况下,第二个线程会在第一个线程不知道的情况下,让
s t r t o k用一个新地址来改写它的静态变量。第一个线程将来调用 s t r t o k时将使用第二个线程的字
符串,这就会导致各种各样难以发现和排除的错误。
为了解决这个问题,C / C + +运行期库使用了T L S。每个线程均被赋予它自己的字符串指针,
供s t r t o k函数使用。需要予以同样对待的其他C / C + +运行期库函数还有a s c t i m e和g m t i m e。
如果你的应用程序需要严重依赖全局变量或静态变量,那么T L S能够帮助解决它遇到的问题。
但是编程人员往往尽可能减少对这些变量的使用,而更多地依赖自动(基于堆栈的)变量和通过
函数的参数传递的数据。这样做是很好的,因为基于堆栈的变量总是与特定的线程相联系的。
标准的C运行期库一直是由许多不同的编译器供应商来实现和重新实现的。如果 C编译器
不包含标准的C运行期库,那么就不值得去购买它。编程员多年来一直使用标准的 C运行期库,
并且将会继续使用它,这意味着s t r t o k之类的函数的原型和行为特性必须与上面所说的标准C运
行期库完全一样。如果今天重新来设计C运行期库,那么它就必须支持多线程应用程序的环境,
并且必须采取相应的措施来避免使用全局变量和静态变量。
在我的软件开发项目中,我总是尽可能避免使用全局变量和静态变量。如果你的应用程序
使用全局变量和静态变量,那么建议你务必观察每个变量,并且了解一下它能否改变成基于堆
栈的变量。如果打算将线程添加给应用程序,那么这样做可以节省大量时间,甚至单线程应用
程序也能够从中得到许多好处。
  回复  更多评论    

# re: 使用线程局部存储TLS  2008-12-12 18:01 阿福

楼上的老兄说的对,其思想也就是在编译器级别支持线程局部存储的意思吧!
比如现在C标准库的errno是这样的定义
#define errno geterrno()
表面上看是一个变量,而实际上去调用一个函数来实现。
在函数里面实现了将数据存储在自己的栈里面。
  回复  更多评论    

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理