TLSAlloc()

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

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

  線程局部存儲在不同的平臺有不同的實現,可移植性不太好。幸好要實現線程局部存儲並不難,最簡單的辦法就是建立一個全局表,通過當前線程ID去查詢相應的數據,因爲各個線程的ID不同,查到的數據自然也不同了。

  大多數平臺都提供了線程局部存儲的方法,無需要我們自己去實現:

  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,更常的用途是放置指針,指向線程的私有資料。有許多情況,多線程程序需要儲存一堆數據,而它們又都是與各線程相關。許多程序員對此的作法是把這些變量包裝爲結構,然後把結構指針儲存在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進行操作的:  

1TlsAlloc  

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

2TlsSetValue  

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

3TlsGetValue  

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

4TlsFree  

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

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取得線程的創建時間,然後返回當前時間和創建時間的差值。

發佈了6 篇原創文章 · 獲贊 31 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章