TLS--线程局部存储
概念:線程局部存儲(Thread Local Storage,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);
?
?功能:它主要是為了避免多個線程同時訪存同一全局變量或者靜態變量時所導致的沖突,尤其是多個線程同時需要修改這一變量時。為了解決這個問題,我們可以通過TLS機制,為每一個使用該全局變量的線程都提供一個變量值的副本,每一個線程均可以獨立地改變自己的副本,而不會和其它線程的副本沖突。從線程的角度看,就好像每一個線程都完全擁有該變量。而從全局變量的角度上來看,就好像一個全局變量被克隆成了多份副本,而每一份副本都可以被一個線程獨立地改變。
?
分類:動態TLS和靜態TLS。
?
用途:動態TLS和靜態TLS這兩項技術在創建DLL的時候更加有用,這是因為DLL通常并不知道它們被鏈接到的應用程序的結構是什么樣的。
1.????????????? 如果應用程序高度依賴全局變量或靜態變量,那么TLS可以成為我們的救生符。因而最好在開發中最大限度地減少對此類變量的使用,更多的依賴于自動變量(棧上的變量)和通過函數參數傳入的數據,因為棧上的變量始終都是與某個特定的線程相關聯的。如果不使用此類變量,那么就可以避免使用TLS。
2.????????????? 但是在編寫應用程序時,我們一般都知道自己要創建多少線程,自己會如何使用這些線程,然后我們就可以設計一些替代方案來為每個線程關聯數據,或者設計得好一點的話,可以使用基于棧的方法(局部變量)來為每個線程關聯數據。
1 動態TLS
系統中每個進程都有一組正在使用標志(in-use flags),每個標志可以被設為FREE或INUSE,表示該TLS元素是否正在被使用。
進程中的線程是通過使用一個數組來保存與線程相關聯的數據的,這個數組由TLS_MINIMUM_AVAILABLE個元素組成,在WINNT.H文件中該值被定義為64個。也就是說當線程創建時,系統給每一個線程分配了一個數組,這個數組共有TLS_MINIMUM_AVAILABLE個元素,并且將這個數組的各個元素初始化為0,之后系統把這個數組與新創建的線程關聯起來。每一個線程中都有它自己的數組,數組中的每一個元素都能保存一個32位的值。在使用這個數組前首先要判定,數組中哪個元素可以使用,這將使用函數TlsAlloc來判斷。函數TlsAlloc判斷數組中一個元素可用后,就把這個元素分配給調用的線程,并保留給調用線程。要為數組中的某個元素賦值可以使用函數TlsSetValue,要得到某個元素的值可以使用TlsGetValue。
?
?一般通過調用一組4個API函數來使用動態TLS:TlsAlloc、TlsSetValue、TlsGetValue和TlsFree。
1)要使用動態TLS,必須先調用TlsAlloc函數:
DWORD?WINAPI?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?//索引值
);
?
這個函數告訴系統已經預定的這個TLS元素現在不需要了,函數會將進程內的位標志數組中對應的INUSE標志重新設回FREE。此外,函數還會將所有線程中該元素的內容設為0.
?
使用動態TLS:
通常,如果DLL要使用TLS,那它會在DllMain函數處理DLL_PROCESS_ATTACH的時候調用TlsAlloc,在DllMain處理DLL_PROCESS_DETACH的時候調用TlsFree。而TlsSetValue和TlsGetValue的調用則最有可能發生在DLL所提供的其他函數中。而向應用程序中添加TLS的一種方法是直到需要時才添加。
?
下面是在應用程序中使用動態TLS的實例代碼:
示例1:
#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;
}
?
?下面的實例代碼是在DLL中使用TLS的:
示例三:
//?The?DLL?code?#include?<windows.h>?
static?DWORD?dwTlsIndex;?//?address?of?shared?memory?
//?DllMain()?is?the?entry-point?function?for?this?DLL.?
BOOL?WINAPI?DllMain(HINSTANCE?hinstDLL,?//?DLL?module?handle
????DWORD?fdwReason,????????????????????//?reason?called
????LPVOID?lpvReserved)?????????????????//?reserved
{
????LPVOID?lpvData;
????BOOL?fIgnore;
?
????switch?(fdwReason)
????{
????????//?The?DLL?is?loading?due?to?process
????????//?initialization?or?a?call?to?LoadLibrary.
?
????????case?DLL_PROCESS_ATTACH:
?
????????????//?Allocate?a?TLS?index.
?
????????????if?((dwTlsIndex?=?TlsAlloc())?==?TLS_OUT_OF_INDEXES)
????????????????return?FALSE;
?
????????????//?No?break:?Initialize?the?index?for?first?thread.
?
????????//?The?attached?process?creates?a?new?thread.
?
????????case?DLL_THREAD_ATTACH:
?
????????????//?Initialize?the?TLS?index?for?this?thread.
?
????????????lpvData?=?(LPVOID)?LocalAlloc(LPTR,?256);
????????????if?(lpvData?!=?NULL)
????????????????fIgnore?=?TlsSetValue(dwTlsIndex,?lpvData);
?
????????????break;
?
????????//?The?thread?of?the?attached?process?terminates.
?
????????case?DLL_THREAD_DETACH:
?
????????????//?Release?the?allocated?memory?for?this?thread.
?
?
?
?
?
轉載于:https://www.cnblogs.com/stli/archive/2010/11/03/1867852.html
總結
以上是生活随笔為你收集整理的TLS--线程局部存储的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: newman执行测试_postman+n
- 下一篇: 寄存器(CPU工作原理)04 - 零基础