線程局部存儲(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;}

相關文章
相關標籤/搜索