為什么要有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,更常的用途是放置指針,指向線程的私有資料。有許多情況,多線程程序需要儲存一堆數據,而它們又都是與各線程相關。許多程序員對此的作法是把這些變量包裝為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取得線程的創建時間,然后返回當前時間和創建時間的差值。
http://www.cnblogs.com/lzjsky/archive/2010/09/01/1814843.html