線程局部存儲(chǔ)(thread-local storage, TLS)是一個(gè)使用很方便的存儲(chǔ)線程局部數(shù)據(jù)的系統(tǒng)。利用TLS機(jī)制可以為進(jìn)程中所有的線程關(guān)聯(lián)若干個(gè)數(shù)據(jù),各個(gè)線程通過(guò)由TLS分配的全局索引來(lái)訪問(wèn)與自己關(guān)聯(lián)的數(shù)據(jù)。這樣,每個(gè)線程都可以有線程局部的靜態(tài)存儲(chǔ)數(shù)據(jù)。
用于管理TLS的數(shù)據(jù)結(jié)構(gòu)是很簡(jiǎn)單的,Windows僅為系統(tǒng)中的每一個(gè)進(jìn)程維護(hù)一個(gè)位數(shù)組,再為該進(jìn)程中的每一個(gè)線程申請(qǐng)一個(gè)同樣長(zhǎng)度的數(shù)組空間,如圖3.9所示。

圖3.9 TSL機(jī)制在內(nèi)部使用的數(shù)據(jù)結(jié)構(gòu)
運(yùn)行在系統(tǒng)中的每一個(gè)進(jìn)程都有圖3.9所示的一個(gè)位數(shù)組。位數(shù)組的成員是一個(gè)標(biāo)志,每個(gè)標(biāo)志的值被設(shè)為FREE或INUSE,指示了此標(biāo)志對(duì)應(yīng)的數(shù)組索引是否在使用中。Windodws保證至少有TLS_MINIMUM_AVAILABLE(定義在WinNT.h文件中)個(gè)標(biāo)志位可用。
動(dòng)態(tài)使用TLS的典型步驟如下。
(1)主線程調(diào)用TlsAlloc函數(shù)為線程局部存儲(chǔ)分配索引,函數(shù)原型為:
DWORD TlsAlloc(void); // 返回一個(gè)TLS索引
如上所述,系統(tǒng)為每一個(gè)進(jìn)程都維護(hù)著一個(gè)長(zhǎng)度為TLS_MINIMUM_AVAILABLE的位數(shù)組,TlsAlloc的返回值就是數(shù)組的一個(gè)下標(biāo)(索引)。這個(gè)位數(shù)組的惟一用途就是記憶哪一個(gè)下標(biāo)在使用中。初始狀態(tài)下,此位數(shù)組成員的值都是FREE,表示未被使用。當(dāng)調(diào)用TlsAlloc的時(shí)候,系統(tǒng)會(huì)挨個(gè)檢查這個(gè)數(shù)組中成員的值,直到找到一個(gè)值為FREE的成員。把找到的成員的值由FREE改為INUSE后,TlsAlloc函數(shù)返回該成員的索引。如果不能找到一個(gè)值為FREE的成員,TlsAlloc函數(shù)就返回TLS_OUT_OF_INDEXES(在WinBase.h文件中定義為-1),意味著失敗。
例如,在第一次調(diào)用TlsAlloc的時(shí)候,系統(tǒng)發(fā)現(xiàn)位數(shù)組中第一個(gè)成員的值是FREE,它就將此成員的值改為INUSE,然后返回0。
當(dāng)一個(gè)線程被創(chuàng)建時(shí),Windows就會(huì)在進(jìn)程地址空間中為該線程分配一個(gè)長(zhǎng)度為TLS_MINIMUM_AVAILABLE的數(shù)組,數(shù)組成員的值都被初始化為0。在內(nèi)部,系統(tǒng)將此數(shù)組與該線程關(guān)聯(lián)起來(lái),保證只能在該線程中訪問(wèn)此數(shù)組中的數(shù)據(jù)。如圖3.7所示,每個(gè)線程都有它自己的數(shù)組,數(shù)組成員可以存儲(chǔ)任何數(shù)據(jù)。
(2)每個(gè)線程調(diào)用TlsSetValue和TlsGetValue設(shè)置或讀取線程數(shù)組中的值,函數(shù)原型為:
BOOL TlsSetValue(
DWORD dwTlsIndex, // TLS 索引
LPVOID lpTlsValue // 要設(shè)置的值
);
LPVOID TlsGetValue(DWORD dwTlsIndex ); // TLS索引
TlsSetValue函數(shù)將參數(shù)lpTlsValue指定的值放入索引為dwTlsIndex的線程數(shù)組成員中。這樣,lpTlsValue的值就與調(diào)用TlsSetValue函數(shù)的線程關(guān)聯(lián)了起來(lái)。此函數(shù)調(diào)用成功,會(huì)返回TRUE。
調(diào)用TlsSetValue函數(shù),一個(gè)線程只能改變自己線程數(shù)組中成員的值,而沒(méi)有辦法為另一個(gè)線程設(shè)置TLS值。到現(xiàn)在為止,將數(shù)據(jù)從一個(gè)線程傳到另一個(gè)線程的惟一方法是在創(chuàng)建線程時(shí)使用線程函數(shù)的參數(shù)。
TlsGetValue函數(shù)的作用是取得線程數(shù)組中索引為dwTlsIndex的成員的值。
TlsSetValue和TlsGetValue分別用于設(shè)置和取得線程數(shù)組中的特定成員的值,而它們使用的索引就是TlsAlloc函數(shù)的返回值。這就充分說(shuō)明了進(jìn)程中惟一的位數(shù)組和各線程數(shù)組的關(guān)系。例如,TlsAlloc返回3,那就說(shuō)明索引3被此進(jìn)程中的每一個(gè)正在運(yùn)行的和以后要被創(chuàng)建的線程保存起來(lái),用以訪問(wèn)各自線程數(shù)組中對(duì)應(yīng)的成員的值。
(3)主線程調(diào)用TlsFree釋放局部存儲(chǔ)索引。函數(shù)的惟一參數(shù)是TlsAlloc返回的索引。
利用TLS可以給特定的線程關(guān)聯(lián)一個(gè)數(shù)據(jù)。比如下面的例子將每個(gè)線程的創(chuàng)建時(shí)間與該線程關(guān)聯(lián)了起來(lái),這樣,在線程終止的時(shí)候就可以得到線程的生命周期。整個(gè)跟蹤線程運(yùn)行時(shí)間的例子的代碼如下:
#include <stdio.h> // 03UseTLS工程下
#include <windows.h>
#include <process.h>
// 利用TLS跟蹤線程的運(yùn)行時(shí)間
DWORD g_tlsUsedTime;
void InitStartTime();
DWORD GetUsedTime();
UINT __stdcall ThreadFunc(LPVOID)
{ int i;
// 初始化開(kāi)始時(shí)間
InitStartTime();
// 模擬長(zhǎng)時(shí)間工作
i = 10000*10000;
while(i--){}
// 打印出本線程運(yùn)行的時(shí)間
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];
// 通過(guò)在進(jìn)程位數(shù)組中申請(qǐng)一個(gè)索引,初始化線程運(yùn)行時(shí)間記錄系統(tǒng)
g_tlsUsedTime = ::TlsAlloc();
// 令十個(gè)線程同時(shí)運(yùn)行,并等待它們各自的輸出結(jié)果
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]); }
// 通過(guò)釋放線程局部存儲(chǔ)索引,釋放時(shí)間記錄系統(tǒng)占用的資源
::TlsFree(g_tlsUsedTime);
return 0;
}
// 初始化線程的開(kāi)始時(shí)間
void InitStartTime()
{ // 獲得當(dāng)前時(shí)間,將線程的創(chuàng)建時(shí)間與線程對(duì)象相關(guān)聯(lián)
DWORD dwStart = ::GetTickCount();
::TlsSetValue(g_tlsUsedTime, (LPVOID)dwStart);
}
// 取得一個(gè)線程已經(jīng)運(yùn)行的時(shí)間
DWORD GetUsedTime()
{ // 獲得當(dāng)前時(shí)間,返回當(dāng)前時(shí)間和線程創(chuàng)建時(shí)間的差值
DWORD dwElapsed = ::GetTickCount();
dwElapsed = dwElapsed - (DWORD)::TlsGetValue(g_tlsUsedTime);
return dwElapsed;
}
GetTickCount函數(shù)可以取得Windows從啟動(dòng)開(kāi)始經(jīng)過(guò)的時(shí)間,其返回值是以毫秒為單位的已啟動(dòng)的時(shí)間。
一般情況下,為各線程分配TLS索引的工作要在主線程中完成,而分配的索引值應(yīng)該保存在全局變量中,以方便各線程訪問(wèn)。上面的例子代碼很清除地說(shuō)明了這一點(diǎn)。主線程一開(kāi)始就使用TlsAlloc為時(shí)間跟蹤系統(tǒng)申請(qǐng)了一個(gè)索引,保存在全局變量g_tlsUsedTime中。之后,為了示例TLS機(jī)制的特點(diǎn)同時(shí)創(chuàng)建了10個(gè)線程。這10個(gè)線程最后都打印出了自己的生命周期,如圖3.10所示。
