线程局部存储(TLS)

线程局部存储(TLS)

2011-10-11 09:59:28|  分类: Win32---API |  标签:tls   |举报 |字号 订阅

什么是线程局部存储

众所周知,线程是执行的单元,同一个进程内的多个线程共享了进程的地址空间,线程一般有自己的栈,但是如果想要实现某个全局变量在不同的线程之间取不同的值,而且不受影响。一种办法是采用线程的同步机制,如对这个变量的读写之处加临界区或者互斥量,但是这是以牺牲效率为代价的,能不能不加锁呢?线程局部存储(TLS)就是干这个的。

虽然TLS 很方便,它并不是毫无限制。在Windows NT 和Windows 95 之中,有64 个DWORD slots 供每一个线程使用。这意思是一个进程最多可以有64 个「对各线程有不同意义」的DWORDs。虽然TLS 可以存放单一数值如文件handle,更常的用途是放置指针,指向线程的私有资料。有许多情况,多线程程序需要储存一堆数据,而它们又都是与各线程相关。许多程序员对此的作法是把这些变量包装为C结构,然后把结构指针储存在TLS 中。当新的线程诞生,程序就配置一些内存给该结构使用,并且把指针储存在为线程保留下来的TLS 中。一旦线程结束,程序代码就释放所有配置来的区块。既然每一个线程都有64个slots 用来储存线程自己的数据,那么这些空间到底打哪儿来?在线程的学习中我们可以从结构TDB中看到,每一个thread database 都有64 个DWORDs 给TLS 使用。

每个线程除了共享进程的资源外还拥有各自的私有资源:一个寄存器组(或者说是线程上下文);一个专属的堆栈;一个专属的消息队列;一个专属的Thread Local Storage(TLS);一个专属的结构化异常处理串链。系统以一个特定的数据结构(Thread Database,TDB)记录执行线程的所有相关资料,包括执行线程局部储存空间(Thread Local Storage,TLS)、消息队列、handle表格、地址空间(Memory Context )等。

当你以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结构)。

应该都知道,操作系统会使用一个结构来描述线程,这结构通常称为TEB((Thread Environment Block) , 每个线程有一个对应的TEB,切换线程的时候,也会切换到不同的TEB。有某个指针值指向当前的TEB, 切换线程的时候就改变这个指针值,这样访问线程相关的数值,就可以统一从这个指针值找起。TEB 里面有些什么变量呢?其中有个变量是线程TLS数组的指针。称为_tls_array,利用这个数组就可以管理线程相关的数据了。我们在不同的线程中已经可以取得各自的_tls_array,这时候,要访问数组的元素,还差索引。这时,再看看TlsAlloc, 你应该很清楚它的意思?没错,它就是说,请为我分配一个索引号,表示相应的数组项已被使用。TlsFree, 就是释放索引号,表示相应的数组项可以被再次使用。TlsSetValue,TlsGetValue就是拿个索引,向相应的数组项设值或者取值。

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

Win32实现

Windows中是根据线程局部存储索引来标识的(这个标识的分配和释放由TlsAlloc和TlsFree完成),有了个这个”标识“就可以在各个线程中调用TlsGetValue或者TlsSetValue读取或者设置各线程各自的值;

(1)首先必须先调用TlsAlloc函数:

DWORD 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了。

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

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

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

(3)为了从线程的数组中取回一个值,应该调用函数TlsGetValue:
     LPVOID WINAPI TlsGetValue(
      __in  DWORD dwTlsIndex //索引值
);

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

(4)当不再需要一个已经预定的TLS元素时,应该调用TlsFree函数:
     BOOL WINAPI TlsFree(
      __in  DWORD dwTlsIndex //索引值
);

Posix实现:与Windows类似,此处的key的作用就相当于上面的dwtlsIndex。

int pthread_key_create(pthread_key_t * key, void (*)(void *));
int pthread_key_delete(pthread_key_t);
void *pthread_getspecific(pthread_key_t);
int pthread_setspecific(pthread_key_t, const void *);

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

常用情景:

例如,你可能有一个多线程程序,每一个线程都对不同的文件写文件(也因此它们使用不同的文件handle)。这种情况下,把每一个线程所使用的文件handle 储存在TLS 中,将会十分方便。当线程需要知道所使用的handle,它可以从TLS 获得。重点在于:线程用来取得文件handle 的那一段码在任何情况下都是相同的,而从TLS中取出的文件handle 却各不相同。非常灵巧,不是吗?有全域变数的便利,却又分属各线程。

下面是在应用程序中使用动态TLS的实例代码:

示例一:

#include <windows.h>
#include <stdio.h>
#define THREADCOUNT 4
DWORD dwTlsIndex;
VOID ErrorExit(LPSTR); 
VOID CommonFunc(VOID)
{
   LPVOID lpvData; 
// Retrieve a data pointer for the current thread. 
   lpvData = TlsGetValue(dwTlsIndex);
   if ((lpvData == 0) && (GetLastError() != ERROR_SUCCESS))
      ErrorExit("TlsGetValue error"); 
// Use the data stored for the current thread. 
   printf("common: thread %d: lpvData=%lx\n",
      GetCurrentThreadId(), lpvData); 
   Sleep(5000);

DWORD WINAPI ThreadFunc(VOID)
{
   LPVOID lpvData; 
// Initialize the TLS index for this thread. 
   lpvData = (LPVOID) LocalAlloc(LPTR, 256);
   if (! TlsSetValue(dwTlsIndex, lpvData))
      ErrorExit("TlsSetValue error"); 
   printf("thread %d: lpvData=%lx\n", GetCurrentThreadId(), lpvData); 
   CommonFunc(); 
// Release the dynamic memory before the thread returns. 
   lpvData = TlsGetValue(dwTlsIndex);
   if (lpvData != 0)
      LocalFree((HLOCAL) lpvData); 
   return 0;

int main(VOID)
{
   DWORD IDThread;
   HANDLE hThread[THREADCOUNT];
   int i; 
// Allocate a TLS index. 
   if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)
      ErrorExit("TlsAlloc failed"); 
// Create multiple threads. 
   for (i = 0; i < THREADCOUNT; i++)
   {
      hThread[i] = CreateThread(NULL, // default security attributes
         0,                           // use default stack size
         (LPTHREAD_START_ROUTINE) ThreadFunc, // thread function
         NULL,                    // no thread function argument
         0,                       // use default creation flags
         &IDThread);              // returns thread identifier 
   // Check the return value for success.
      if (hThread[i] == NULL)
         ErrorExit("CreateThread error\n");
   } 
   for (i = 0; i < THREADCOUNT; i++)
      WaitForSingleObject(hThread[i], INFINITE); 
   TlsFree(dwTlsIndex); 
   return 0;
}
VOID ErrorExit (LPSTR lpszMessage)
{
   fprintf(stderr, "%s\n", lpszMessage);
   ExitProcess(0);
}

示例二:

#include <stdio.h>
#include <windows.h>
#include <process.h>

// 利用TLS记录线程的运行时间

DWORD g_tlsUsedTime;
void InitStartTime();
DWORD GetUsedTime();

UINT __stdcall ThreadFunc(LPVOID)
{
  int i;

// 初始化开始时间
  InitStartTime();

// 模拟长时间工作
  i = 10000*10000;
  while(i--) { }

// 打印出本线程运行的时间
  printf(" This thread is coming to end. Thread ID: %-5d, Used Time: %d \n", 
            ::GetCurrentThreadId(), GetUsedTime());
  return 0;
}

int main(int argc, char* argv[])
{
  UINT uId;
  int i;
  HANDLE h[10];

// 通过在进程位数组中申请一个索引,初始化线程运行时间记录系统
   g_tlsUsedTime = ::TlsAlloc();

// 令十个线程同时运行,并等待它们各自的输出结果
  for(i=0; i<10; i++)
  {
    h[i] = (HANDLE)::_beginthreadex(NULL, 0, ThreadFunc, NULL, 0, &uId);
  }
  for(i=0; i<10; i++)
  {
    ::WaitForSingleObject(h[i], INFINITE);
    ::CloseHandle(h[i]);
  }

// 通过释放线程局部存储索引,释放时间记录系统占用的资源
  ::TlsFree(g_tlsUsedTime);
  return 0;
}

// 初始化线程的开始时间
void InitStartTime()
{
  // 获得当前时间,将线程的创建时间与线程对象相关联
  DWORD dwStart = ::GetTickCount();
  ::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);
}

// 取得一个线程已经运行的时间
DWORD GetUsedTime()
{
  // 获得当前时间,返回当前时间和线程创建时间的差值
  DWORD dwElapsed = ::GetTickCount();
  dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);
  return dwElapsed;
}

时间: 2024-11-10 18:11:23

线程局部存储(TLS)的相关文章

【windows核心编程】线程局部存储TLS

线程局部存储TLS, Thread Local Storage TLS是C/C++运行库的一部分,而非操作系统的一部分. 分为动态TSL 和 静态TLS 一.动态TLS 应用程序通过调用一组4个函数来使用动态TLS, 这些函数实际上最为DLL所使用. 系统中的每个进程都有一组 正在使用标志(in-use flag), 每个标志可被设置为FREE 或者 INUSE, 表示该TLS元素是否正在使用. 微软平台保证至少有TLS_MINUMUM_AVALIABLE个标志位可供使用, TLS_MINUMU

线程局部存储 TLS

C/C++运行库提供了TLS(线程局部存储),在多线程还未产生时,可以将数据与正在执行的线程关联.strtok()函数就是一个很好的例子.与它一起的还有strtok_s(),_tcstok_s()等等函数,其实_tcs 是 wcs 的另外一种写法,表示宽字符存储,_s 是微软定义的安全函数,通常比普通函数多一个参数.以_tcstok_s()为例, int main(int argc, char* argv[]){ wchar_t Source[] = L"192.168.255.255"

线程局部存储TLS

1 .使用线程局部存储的理由 当我们希望这个进程的全局变量变为线程私有时,而不是所有线程共享的,也就是每个线程拥有一份副本时,这时候就可以用到线程局部存储(TLS,Thread Local Storage)这个机制了. 2.动态TLS(1)调用TlsAlloc函数  两种方式: 1>全局调用一次:   global_dwTLSindex=TLSAlloc(); 如果程序只调用一次TlsAlloc,而不是每个线程都调用一次TlsAlloc()函数的话,多个线程就使用同一个索引值,不同线程虽然看起来

TLS 线程局部存储

thread local storage TLS 线程局部存储,布布扣,bubuko.com

基于TLS(线程局部存储)的高效timelog实现

什么是timelog? 我们在分析程序性能的时候,会加入的一些logging信息记录每一部分的时间信息 timelog模块的功能就是提供统一的接口来允许添加和保存logging 我们正在用的timelog有几个缺点 1.固定大小,一旦满了就不能加入新的logging 2.每次进入就会有一个全局的lock锁住,非常影响性能 这两天基于boost的thread_specific_ptr和circular_buffer实现了一个高效的timelog,主要特点有 1. 几乎不需要任何lock,所以性能理

每天进步一点点——Linux中的线程局部存储(一)

转载请说明出处:http://blog.csdn.net/cywosp/article/details/26469435 在Linux系统中使用C/C++进行多线程编程时,我们遇到最多的就是对同一变量的多线程读写问题,大多情况下遇到这类问题都是通过锁机制来处理,但这对程序的性能带来了很大的影响,当然对于那些系统原生支持原子操作的数据类型来说,我们可以使用原子操作来处理,这能对程序的性能会得到一定的提高.那么对于那些系统不支持原子操作的自定义数据类型,在不使用锁的情况下如何做到线程安全呢?本文将从

设计自己的线程局部存储

参考资料 王艳平 <Windows程序设计> #pragma once #include <windows.h> #include <stddef.h> #include<iostream> using namespace std; class CNoTrackObject; class CSimpleList//将每个线程的私有数据的首地址串联起来 { public: CSimpleList(int NextOffset = 0); void Constr

第21章 线程局部存储区

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

使用线程局部存储实现多线程下的日志系统(转)

http://www.ibm.com/developerworks/cn/linux/1310_qianbh_threadlog/index.html 多线程编程向来不容易,在多线程环境下实现日志系统是很多程序员亟须解决的问题.在本文中详细介绍了线程局部存储的概念.原理,并用代码示例详细展示了如何使用线程局部存储来实现多线程下的日志系统. 概述 通常来说,在应用程序中需要日志来记录程序运行的状态,以便后期问题的跟踪定位.在日志系统的设计中,通常会有一个总的日志系统来统一协调这些日志的设置如位置.