线程局部存储(Thread Local StorageTLS)用来将数据与一个正在执行的指定线程关联起来。我们在应用程序和DLL中可以使用两种类型的TLS:动态TLS和静态TLS。但一般来说,这两项技术在创建DLL的时候更加有用,这是因为DLL通常并不知道它们被链接到的应用程序的结构是什么样的。但是在编写应用程序时,我们一般都知道自己要创建多少线程,自己会如何使用这些线程。然后我们就可以设计一些替代方案来为每个线程关联数据,或者设计得好一点的话,可以使用基于栈的方法(局部变量)来为每个线程关联数据。


动态TLS

一般通过调用一组4API函数来使用动态TLS,这些函数实际上最经常为DLL使用。系统中每个进程都有一组正在使用标志(in-use flags),每个标志可以被设为FREEINUSE,表示该TLS元素是否正在被使用。Microsoft保证至少TLS_MINIMUM_AVAILABLE个位标志可供使用。其中TLS_MINIMUM_AVAILABLEWinNT.h中被定义为64,系统会在需要时分配更多的TLS元素,最多可达1000多个!

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

DWORD WINAPI TlsAlloc(void);

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

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

当系统创建一个线程的时候,会分配TLS_MINIMUM_AVAILABLEPVOID值,将它们都初始化为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 //索引值

);

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


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

BOOL WINAPI TlsFree(

  __in  DWORD dwTlsIndex //索引值

);

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


使用动态TLS

通常,如果DLL要使用TLS,那它会在DllMain函数处理DLL_PROCESS_ATTACH的时候调用TlsAlloc,在DllMain处理DLL_PROCESS_DETACH的时候调用TlsFree。而TlsSetValueTlsGetValue的调用则最有可能发生在DLL所提供的其他函数中。


而向应用程序中添加TLS的一种方法是直到需要时才添加。


下面是在应用程序中使用动态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);

}


下面的实例代码是在DLL中使用TLS的:

========================DLL代码==========================

// The DLL code


#include <windows.h>


static DWORD dwTlsIndex; // address of shared memory


// DllMain() is the entry-point function for this DLL.


BOOL WINAPI DllMain(HINSTANCE hinstDLL, // DLL module handle

    DWORD fdwReason,                    // reason called

    LPVOID lpvReserved)                 // reserved

{

    LPVOID lpvData;

    BOOL fIgnore;


    switch (fdwReason)

    {

        // The DLL is loading due to process

        // initialization or a call to LoadLibrary.


        case DLL_PROCESS_ATTACH:


            // Allocate a TLS index.


            if ((dwTlsIndex = TlsAlloc()) == TLS_OUT_OF_INDEXES)

                return FALSE;


            // No break: Initialize the index for first thread.


        // The attached process creates a new thread.


        case DLL_THREAD_ATTACH:


            // Initialize the TLS index for this thread.


            lpvData = (LPVOID) LocalAlloc(LPTR, 256);

            if (lpvData != NULL)

                fIgnore = TlsSetValue(dwTlsIndex, lpvData);


            break;


        // The thread of the attached process terminates.


        case DLL_THREAD_DETACH:


            // Release the allocated memory for this thread.


            lpvData = TlsGetValue(dwTlsIndex);

            if (lpvData != NULL)

                LocalFree((HLOCAL) lpvData);


            break;


        // DLL unload due to process termination or FreeLibrary.


        case DLL_PROCESS_DETACH:


            // Release the allocated memory for this thread.


            lpvData = TlsGetValue(dwTlsIndex);

            if (lpvData != NULL)

                LocalFree((HLOCAL) lpvData);


            // Release the TLS index.


            TlsFree(dwTlsIndex);

            break;


        default:

            break;

    }


    return TRUE;

    UNREFERENCED_PARAMETER(hinstDLL);

    UNREFERENCED_PARAMETER(lpvReserved);

}


// The export mechanism used here is the __declspec(export)

// method supported by Microsoft Visual Studio, but any

// other export method supported by your development

// environment may be substituted.


#ifdef __cplusplus    // If used by C++ code,

extern "C" {          // we need to export the C interface

#endif


__declspec(dllexport)

BOOL WINAPI StoreData(DWORD dw)

{

   LPVOID lpvData;

   DWORD * pData;  // The stored memory pointer


   lpvData = TlsGetValue(dwTlsIndex);

   if (lpvData == NULL)

   {

      lpvData = (LPVOID) LocalAlloc(LPTR, 256);

      if (lpvData == NULL)

         return FALSE;

      if (!TlsSetValue(dwTlsIndex, lpvData))

         return FALSE;

   }


   pData = (DWORD *) lpvData; // Cast to my data type.

   // In this example, it is only a pointer to a DWORD

   // but it can be a structure pointer to contain more complicated data.


   (*pData) = dw;

   return TRUE;

}


__declspec(dllexport)

BOOL WINAPI GetData(DWORD *pdw)

{

   LPVOID lpvData;

   DWORD * pData;  // The stored memory pointer


   lpvData = TlsGetValue(dwTlsIndex);

   if (lpvData == NULL)

      return FALSE;


   pData = (DWORD *) lpvData;

   (*pdw) = (*pData);

   return TRUE;

}

#ifdef __cplusplus

}

#endif


====================使用上述DLL的代码========================

#include <windows.h>

#include <stdio.h>


#define THREADCOUNT 4

#define DLL_NAME TEXT("testdll")


VOID ErrorExit(LPSTR);


extern "C" BOOL WINAPI StoreData(DWORD dw);

extern "C" BOOL WINAPI GetData(DWORD *pdw);


DWORD WINAPI ThreadFunc(VOID)

{  

   int i;


   if(!StoreData(GetCurrentThreadId()))

      ErrorExit("StoreData error");


   for(i=0; i<THREADCOUNT; i++)

   {

      DWORD dwOut;

      if(!GetData(&dwOut))

         ErrorExit("GetData error");

      if( dwOut != GetCurrentThreadId())

         printf("thread %d: data is incorrect (%d)/n", GetCurrentThreadId(), dwOut);

      else printf("thread %d: data is correct/n", GetCurrentThreadId());

      Sleep(0);

   }

   return 0;

}


int main(VOID)

{

   DWORD IDThread;

   HANDLE hThread[THREADCOUNT];

   int i;

   HMODULE hm;


// Load the DLL


   hm = LoadLibrary(DLL_NAME);

   if(!hm)

   {

      ErrorExit("DLL failed to load");

   }


// 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");

   }


   WaitForMultipleObjects(THREADCOUNT, hThread, TRUE, INFINITE);


   FreeLibrary(hm);


   return 0;

}


VOID ErrorExit (LPSTR lpszMessage)

{

   fprintf(stderr, "%s/n", lpszMessage);

   ExitProcess(0);

}


静态TLS

与动态TLS相似,静态TLS也将数据与线程关联起来。但是,由于使用时不必在代码中调用任何函数,因此静态TLS更容易使用。

假设想将应用程序创建的每个线程与该线程的启动时间关联起来。我们要做的就是像下面一样声明一个启动时间:

__declspec(thread) DWORD gt_dwStartTime = 0;

前缀__declspec(thread)MicrosoftVisual C++编译器增加的一个修饰符。他/她告诉编译器应该在可执行文件或DLL文件中,把对应的变量放到它自己的段中。__declspec(thread)后面的变量必须被声明为静态变量或全局变量(即可以在函数内,也可以在函数外),但不能被声明为局部变量。

当编译器对程序进行编译时,会将所有TLS变量放到它们自己的段中,这个段名为.tls。链接器会将所有对象模块中的.tls段合并成一个大的.tls段,并将它保存到生成的可执行文件或DLL文件中。

当系统将应用程序载入到内存中时,会查看可执行文件中的.tls段,并分配一块足够大的内存来保存所有的静态TLS变量。每当应用程序中的代码引用到这些变量之一时,相应的引用会被解析到刚分配的这款内存中的一个位置。

因此,编译器必须生成额外的代码来引用静态TLS变量,这使得应用程序不仅变得更大,而且执行起来更慢。在x86CPU上,每次引用一个静态TLS变量会生成三条额外的机器指令。

如果进程创建一个新线程,那么系统会获知这一情况并自动分配另一块内存来保存新线程的静态TLS变量。新线程只能访问自己的静态TLS变量,它无法访问属于任何其他线程的TLS变量。