线程局部存储(TLS)

线程局部存储(TLS)  

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

 

什么是线程局部存储windows

    众所周知,线程是执行的单元,同一个进程内的多个线程共享了进程的地址空间,线程通常有本身的栈,可是若是想要实现某个全局变量在不一样的线程之间取不一样的值,并且不受影响。一种办法是采用线程的同步机制,如对这个变量的读写之处加临界区或者互斥量,可是这是以牺牲效率为代价的,能不能不加锁呢?线程局部存储(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结构)。 ide

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

 线程局部存储在不一样的平台有不一样的实现,可移植性不太好。幸亏要实现线程局部存储并不难,最简单的办法就是创建一个全局表,经过当前线程ID去查询相应的数据,由于各个线程的ID不一样,查到的数据天然也不一样了。大多数平台都提供了线程局部存储的方法,无须要咱们本身去实现:学习

 

Win32实现this

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;}

相关文章
相关标签/搜索