线程局部存储(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;
}