線程局部靜態變量

爲何要有TLS?緣由在於,進程中的全局變量與函數內定義的靜態(static)變量,是各個線程均可以訪問的共享變量。在一個線程修改的內存內容,對全部線程都生效。這是一個優勢也是一個缺點。說它是優勢,線程的數據交換變得很是快捷。說它是缺點,一個線程死掉了,其它線程也性命不保; 多個線程訪問共享數據,須要昂貴的同步開銷,也容易形成同步相關的BUG。linux

  若是須要在一個線程內部的各個函數調用都能訪問、但其它線程不能訪問的變量(被稱爲static memory local to a thread 線程局部靜態變量),就須要新的機制來實現。這就是TLS。程序員

  線程局部存儲在不一樣的平臺有不一樣的實現,可移植性不太好。幸虧要實現線程局部存儲並不難,最簡單的辦法就是創建一個全局表,經過當前線程ID去查詢相應的數據,由於各個線程的ID不一樣,查到的數據天然也不一樣了。編程

  大多數平臺都提供了線程局部存儲的方法,無須要咱們本身去實現:windows

  linux:數組

  int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));安全

  int pthread_key_delete(pthread_key_t key);數據結構

  void *pthread_getspecific(pthread_key_t key);多線程

  int pthread_setspecific(pthread_key_t key, const void *value);函數

  Win32學習

  方法一:每一個線程建立時系統給它分配一個LPVOID指針的數組(叫作TLS數組),這個數組從C編程角度是隱藏着的不能直接訪問,須要經過一些C API函數調用訪問。首先定義一些DWORD線程全局變量或函數靜態變量,準備做爲各個線程訪問本身的TLS數組的索引變量。一個線程使用TLS時,第一步在線程內調用TlsAlloc()函數,爲一個TLS數組索引變量與這個線程的TLS數組的某個槽(slot)關聯起來,例如得到一個索引變量:

  global_dwTLSindex=TLSAlloc();

  注意,此步以後,當前線程實際上訪問的是這個TLS數組索引變量的線程內的拷貝版本。也就說,不一樣線程雖然看起來用的是同名的TLS數組索引變量,但實際上各個線程獲得的多是不一樣DWORD值。其意義在於,每一個使用TLS的線程得到了一個DWORD類型的線程局部靜態變量做爲TLS數組的索引變量。C/C++本來沒有直接定義線程局部靜態變量的機制,因此在如此大費周折。

  第二步,爲當前線程動態分配一塊內存區域(使用LocalAlloc()函數調用),而後把指向這塊內存區域的指針放入TLS數組相應的槽中(使用TlsValue()函數調用)。

  第三步,在當前線程的任何函數內,均可以經過TLS數組的索引變量,使用TlsGetValue()函數獲得上一步的那塊內存區域的指針,而後就能夠進行內存區域的讀寫操做了。這就實現了在一個線程內部這個範圍到處可訪問的變量。

  最後,若是再也不須要上述線程局部靜態變量,要動態釋放掉這塊內存區域(使用LocalFree()函數),而後從TLS數組中放棄對應的槽(使用TlsFree()函數)。

 

 

TLS 是一個良好的Win32 特質,讓多線程程序設計更容易一些。TLS 是一個機制,經由它,程序能夠擁有全域變量,但處於「每一線程各不相同」的狀態。也就是說,進程中的全部線程均可以擁有全域變量,但這些變量實際上是特定對某個線程纔有意義。例如,你可能有一個多線程程序,每個線程都對不一樣的文件寫文件(也所以它們使用不一樣的文件handle)。這種狀況下,把每個線程所使用的文件handle 儲存在TLS 中,將會十分方便。當線程須要知道所使用的handle,它能夠從TLS 得到。重點在於:線程用來取得文件handle 的那一段碼在任何狀況下都是相同的,而從TLS中取出的文件handle 卻各不相同。很是靈巧,不是嗎?有全域變數的便利,卻又分屬各線程。  
 

  雖然TLS 很方便,它並非毫無限制。在Windows NT 和Windows 95 之中,有64 個DWORD slots 供每個線程使用。這意思是一個進程最多能夠有64 個「對各線程有不一樣意義」的DWORDs。 雖然TLS 能夠存放單一數值如文件handle,更常的用途是放置指針,指向線程的私有資料。有許多狀況,多線程程序須要儲存一堆數據,而它們又都是與各線程相關。許多程序員對此的做法是把這些變量包裝爲C 結構,而後把結構指針儲存在TLS 中。當新的線程誕生,程序就配置一些內存給該結構使用,而且把指針儲存在爲線程保留下來的TLS 中。一旦線程結束,程序代碼就釋放全部配置來的區塊。既然每個線程都有64 個slots 用來儲存線程本身的數據,那麼這些空間到底打哪兒來?在線程的學習中咱們能夠從結構TDB中看到,每個thread database 都有64 個DWORDs 給TLS 使用。當你以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結構中咱們列出了那兩個DWORDs)。 
 

下面的四個函數就是對TLS進行操做的:  

(1)TlsAlloc  

上面咱們說過了KERNEL32 使用兩個DWORDs(總共64 個位)來記錄哪個slot 是可用的、哪個slot 已經被用。當你須要使用一個TLS slot 的時候,你就能夠用這個函數將相應的TLS slot位置1。  

(2)TlsSetValue  

TlsSetValue 能夠把數據放入先前配置到的TLS slot 中。兩個參數分別是TLS slot 索引值以及欲寫入的數據內容。TlsSetValue 就把你指定的數據放入64 DWORDs 所組成的數組(位於目前的thread database)的適當位置中。  

(3)TlsGetValue  

這個函數幾乎是TlsSetValue 的一面鏡子,最大的差別是它取出數據而非設定數據。和TlsSetValue 同樣,這個函數也是先檢查TLS 索引值合法與否。若是是,TlsGetValue 就使用這個索引值找到64 DWORDs 數組(位於thread database 中)的對應數據項,並將其內容傳回。  

(4)TlsFree  

這個函數將TlsAlloc TlsSetValue 的努力所有抹消掉。TlsFree 先檢驗你交給它的索引值是否的確被配置過。若是是,它將對應的64 位TLS slots 位關閉。而後,爲了不那個已經再也不合法的內容被使用,TlsFree 巡訪進程中的每個線程,把0 放到剛剛被釋放的那個TLS slot 上頭。因而呢,若是有某個TLS 索引後來又被從新配置,全部用到該索引的線程就保證會取回一個0 值,除非它們再調用TlsSetValue

 

 

互斥(Mutex)是一種用途很是普遍的內核對象。可以保證多個線程對同一共享資源的互斥訪問。同臨界區有些相似,只有擁有互斥對象的線程才具備訪問資源的權限,因爲互斥對象只有一個,所以就決定了任何狀況下此共享資源都不會同時被多個線程所訪問。當前佔據資源的線程在任務處理完後應將擁有的互斥對象交出,以便其餘線程在得到後得以訪問資源。與其餘幾種內核對象不一樣,互斥對象在操做系統中擁有特殊代碼,並由操做系統來管理,操做系統甚至還容許其進行一些其餘內核對象所不能進行的很是規操做。爲便於理解,可參照圖3.8給出的互斥內核對象的工做模型:

 

圖3.8 使用互斥內核對象對共享資源的保護

圖(a)中的箭頭爲要訪問資源(矩形框)的線程,但只有第二個線程擁有互斥對象(黑點)並得以進入到共享資源,而其餘線程則會被排斥在外(如圖(b)所示)。當此線程處理完共享資源並準備離開此區域時將把其所擁有的互斥對象交出(如圖(c)所示),其餘任何一個試圖訪問此資源的線程都有機會獲得此互斥對象。

以互斥內核對象來保持線程同步可能用到的函數主要有CreateMutex、OpenMutex、ReleaseMutex、WaitForSingleObject和WaitForMultipleObjects等。在使用互斥對象前,首先要經過CreateMutex或OpenMutex建立或打開一個互斥對象。CreateMutex函數原型以下:

HANDLE CreateMutex(

 LPSECURITY_ATTRIBUTES lpMutexAttributes,     // 安全屬性指針

 BOOL bInitialOwner,                                            // 初始擁有者

 LPCTSTR lpName                                               // 互斥對象名

);

參數bInitialOwner主要用來控制互斥對象的初始狀態。通常多將其設置爲FALSE,以代表互斥對象在建立時並無爲任何線程所佔有。若是在建立互斥對象時指定了對象名,那麼能夠在本進程其餘地方或是在其餘進程經過OpenMutex函數獲得此互斥對象的句柄。OpenMutex函數原型爲:

HANDLE OpenMutex(
 DWORD dwDesiredAccess, // 訪問標誌
 BOOL bInheritHandle, // 繼承標誌
 LPCTSTR lpName // 互斥對象名
);

當目前對資源具備訪問權的線程再也不須要訪問此資源而要離開時,必須經過ReleaseMutex函數來釋放其擁有的互斥對象,其函數原型爲:

BOOL ReleaseMutex(HANDLE hMutex);

其唯一的參數hMutex爲待釋放的互斥對象句柄。至於WaitForSingleObject和WaitForMultipleObjects等待函數在互斥對象保持線程同步中所起的做用與在其餘內核對象中的做用是基本一致的,也是等待互斥內核對象的通知。可是這裏須要特別指出的是:在互斥對象通知引發調用等待函數返回時,等待函數的返回值再也不是一般的WAIT_OBJECT_0(對於WaitForSingleObject函數)或是在WAIT_OBJECT_0到WAIT_OBJECT_0+nCount-1之間的一個值(對於WaitForMultipleObjects函數),而是將返回一個WAIT_ABANDONED_0(對於WaitForSingleObject函數)或是在WAIT_ABANDONED_0到WAIT_ABANDONED_0+nCount-1之間的一個值(對於WaitForMultipleObjects函數),以此來代表線程正在等待的互斥對象由另一個線程所擁有,而此線程卻在使用完共享資源前就已經終止。除此以外,使用互斥對象的方法在等待線程的可調度性上同使用其餘幾種內核對象的方法也有所不一樣,其餘內核對象在沒有獲得通知時,受調用等待函數的做用,線程將會掛起,同時失去可調度性,而使用互斥的方法卻能夠在等待的同時仍具備可調度性,這也正是互斥對象所能完成的很是規操做之一。
  在編寫程序時,互斥對象多用在對那些爲多個線程所訪問的內存塊的保護上,能夠確保任何線程在處理此內存塊時都對其擁有可靠的獨佔訪問權。下面給出的示例代碼即經過互斥內核對象hMutex對共享內存快g_cArray[]進行線程的獨佔訪問保護。下面是示例代碼:

// 互斥對象

HANDLE hMutex = NULL;

char g_cArray[10];

UINT ThreadProc1(LPVOID pParam)

{

 // 等待互斥對象通知

 WaitForSingleObject(hMutex, INFINITE);

 // 對共享資源進行寫入操做

 for (int i = 0; i < 10; i++)

 {

  g_cArray[i] = 'a';

  Sleep(1);

 }

 // 釋放互斥對象

 ReleaseMutex(hMutex);

 return 0;

}

UINT ThreadProc2(LPVOID pParam)

{

 // 等待互斥對象通知

 WaitForSingleObject(hMutex, INFINITE);

 // 對共享資源進行寫入操做

 for (int i = 0; i < 10; i++)

 {

  g_cArray[10 - i - 1] = 'b';

  Sleep(1);

 }

 // 釋放互斥對象

 ReleaseMutex(hMutex);

 return 0;

}

線程的使用使程序處理可以更加靈活,而這種靈活一樣也會帶來各類不肯定性的可能。尤爲是在多個線程對同一公共變量進行訪問時。雖然未使用線程同步的程序代碼在邏輯上或許沒有什麼問題,但爲了確保程序的正確、可靠運行,必須在適當的場合採起線程同步措施。

3.2.6 線程局部存儲

線程局部存儲(thread-local storage, TLS)是一個使用很方便的存儲線程局部數據的系統。利用TLS機制能夠爲進程中全部的線程關聯若干個數據,各個線程經過由TLS分配的全局索引來訪問與本身關聯的數據。這樣,每一個線程均可以有線程局部的靜態存儲數據。

用於管理TLS的數據結構是很簡單的,Windows僅爲系統中的每個進程維護一個位數組,再爲該進程中的每個線程申請一個一樣長度的數組空間,如圖3.9所示。

 

圖3.9 TSL機制在內部使用的數據結構

運行在系統中的每個進程都有圖3.9所示的一個位數組。位數組的成員是一個標誌,每一個標誌的值被設爲FREE或INUSE,指示了此標誌對應的數組索引是否在使用中。Windodws保證至少有TLS_MINIMUM_AVAILABLE(定義在WinNT.h文件中)個標誌位可用。

動態使用TLS的典型步驟以下。

(1)主線程調用TlsAlloc函數爲線程局部存儲分配索引,函數原型爲:

DWORD TlsAlloc(void); // 返回一個TLS索引

如上所述,系統爲每個進程都維護着一個長度爲TLS_MINIMUM_AVAILABLE的位數組,TlsAlloc的返回值就是數組的一個下標(索引)。這個位數組的唯一用途就是記憶哪個下標在使用中。初始狀態下,此位數組成員的值都是FREE,表示未被使用。當調用TlsAlloc的時候,系統會挨個檢查這個數組中成員的值,直到找到一個值爲FREE的成員。把找到的成員的值由FREE改成INUSE後,TlsAlloc函數返回該成員的索引。若是不能找到一個值爲FREE的成員,TlsAlloc函數就返回TLS_OUT_OF_INDEXES(在WinBase.h文件中定義爲-1),意味着失敗。

例如,在第一次調用TlsAlloc的時候,系統發現位數組中第一個成員的值是FREE,它就將此成員的值改成INUSE,而後返回0。

當一個線程被建立時,Windows就會在進程地址空間中爲該線程分配一個長度爲TLS_MINIMUM_AVAILABLE的數組,數組成員的值都被初始化爲0。在內部,系統將此數組與該線程關聯起來,保證只能在該線程中訪問此數組中的數據。如圖3.7所示,每一個線程都有它本身的數組,數組成員能夠存儲任何數據。

(2)每一個線程調用TlsSetValue和TlsGetValue設置或讀取線程數組中的值,函數原型爲:

BOOL TlsSetValue(

DWORD dwTlsIndex,     // TLS 索引

LPVOID lpTlsValue                   // 要設置的值

);

LPVOID TlsGetValue(DWORD dwTlsIndex );       // TLS索引

TlsSetValue函數將參數lpTlsValue指定的值放入索引爲dwTlsIndex的線程數組成員中。這樣,lpTlsValue的值就與調用TlsSetValue函數的線程關聯了起來。此函數調用成功,會返回TRUE。

調用TlsSetValue函數,一個線程只能改變本身線程數組中成員的值,而沒有辦法爲另外一個線程設置TLS值。到如今爲止,將數據從一個線程傳到另外一個線程的唯一方法是在建立線程時使用線程函數的參數。

TlsGetValue函數的做用是取得線程數組中索引爲dwTlsIndex的成員的值。

TlsSetValue和TlsGetValue分別用於設置和取得線程數組中的特定成員的值,而它們使用的索引就是TlsAlloc函數的返回值。這就充分說明了進程中唯一的位數組和各線程數組的關係。例如,TlsAlloc返回3,那就說明索引3被此進程中的每個正在運行的和之後要被建立的線程保存起來,用以訪問各自線程數組中對應的成員的值。

(3)主線程調用TlsFree釋放局部存儲索引。函數的唯一參數是TlsAlloc返回的索引。

利用TLS能夠給特定的線程關聯一個數據。好比下面的例子將每一個線程的建立時間與該線程關聯了起來,這樣,在線程終止的時候就能夠獲得線程的生命週期。整個跟蹤線程運行時間的例子的代碼以下:

#include <stdio.h>                                   // 03UseTLS工程下

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

}

GetTickCount函數能夠取得Windows從啓動開始通過的時間,其返回值是以毫秒爲單位的已啓動的時間。

通常狀況下,爲各線程分配TLS索引的工做要在主線程中完成,而分配的索引值應該保存在全局變量中,以方便各線程訪問。上面的例子代碼很清除地說明了這一點。主線程一開始就使用TlsAlloc爲時間跟蹤系統申請了一個索引,保存在全局變量g_tlsUsedTime中。以後,爲了示例TLS機制的特色同時建立了10個線程。這10個線程最後都打印出了本身的生命週期,如圖3.10所示。

 

3.10 各線程的生命週期

這個簡單的線程運行時間記錄系統僅提供InitStartTime和GetUsedTime兩個函數供用戶使用。應該在線程一開始就調用InitStartTime函數,此函數獲得當前時間後,調用TlsSetValue將線程的建立時間保存在以g_tlsUsedTime爲索引的線程數組中。當想查看線程的運行時間時,直接調用GetUsedTime函數就好了。這個函數使用TlsGetValue取得線程的建立時間,而後返回當前時間和建立時間的差值。

相關文章
相關標籤/搜索