1. 程式人生 > 實用技巧 >回爐重造之重讀Windows核心程式設計-021-執行緒本地儲存

回爐重造之重讀Windows核心程式設計-021-執行緒本地儲存

第21章 執行緒本地儲存

21.0 簡介

將資料和物件的例項聯絡起來是很有用的,比如C庫函式strtok。當strtok被一個執行緒呼叫之後,又被另一個執行緒搶佔呼叫了一次,第二個執行緒的呼叫就會發生錯誤。而且這種錯誤是很難被發現和排除的。

由於CC++執行時庫出現的時候大多數應用是基於單執行緒的應用程式的,並不會考慮這個。為了解決這個問題,執行時庫就需要給每一個執行緒都配備一個單獨的字串指標,就不會發生這樣的錯誤了,像strtok、asctime、gmtime這類的函式就不會有上述的問題了。

解決這個問題的特性就是執行緒本地儲存(Thread Local Storage, TLS)。這個特性同樣可以讓你的執行緒減少對靜態變數和全域性變數的使用。和執行緒聯絡起來意味著執行緒的生命週期結束時,這些資源也有機會被銷燬。

在編寫應用程式的時候,有2種使用TLS的方法,就是動態TLS和靜態TLS。

21.1 動態TLS

若要使用動態的TLS,可以呼叫一組函式(4個)。這些函式實際上是DLL使用的最多。系統中的執行緒會使用一組標誌,可以是FREE或INUSE,表示是否在使用。從Win2K開始,系統就保證有TLS_MINIMUN_AVAILABLE(2000)個記憶體塊可是使用。

21.1.1動態TLS

若要使用動態TLS,首先呼叫一個函式:

DWORD TlsAlloc();

這個函式對程序中的位標誌進行掃描,並找出第一個FREE標誌。然後將這個標誌從FREE改成INUSE,最後返回一個索引。這個索引被儲存在一個全域性變數中。

至於剩下的1%:當建立一個執行緒時,便分配一個TLS_MINIMUM_AVAILABLEPVOID值的陣列,並將其初始化為0,由系統將它和執行緒關聯起來。這樣每個執行緒都可以得到自己的陣列,陣列中的PVOID可以儲存任何值。

這樣設計的方式體現出來,執行緒可以使用TlsAlloc特意保留的一個索引。若要將一個值放入執行緒的陣列中,可以呼叫TlsValue函式。

BOOL TlsSetValue(
	DWORD dwTlsIndex,
  PVOID pvTlsValue);

這個函式將一個PVOID值(pvTlsValue引數)放入執行緒的陣列中由dwTlsIndex引數表示的索引處。pvTlsValue的值與呼叫TlsSetValue的執行緒想聯絡。如果呼叫成功,就返回TRUE。

執行緒呼叫TlsSetValue的時候只改變自己的陣列,不會對其他執行緒的陣列造成影響。如果想要對其他執行緒傳送一些資料,可以在呼叫CreateThread或者_beginthreadex,然後還函式將資訊作為執行緒函式的引數傳遞給CreateThread或者_begingthreadex函式。

考慮到可能有多次TlsAlloc函式的呼叫,則當呼叫TlsSetValue之時始終都應該有較早時候呼叫TlsAlloc函式返回的索引。對於這些函式Microsoft設計成可以儘快執行,放棄錯誤檢查。

若要從執行緒的陣列中檢索出一個數組的索引,可以呼叫TlGetValue:

PVOID TlsGetValue(DWORD dwTlsIndex);

當在所有執行緒中的不再需要保留TLS時隙的時候,應該呼叫TlsFree:

BOOL TlsFree(DWORD dwTlsIndex);

這個函式簡單地告訴系統這個時隙不再需要保留。那麼由程序管理的位標誌陣列中對應的標誌就從INUSE變成了FREE。這個函式如果執行成功就返回TRUE,如果失敗講返回一個錯誤。

21.1.2 使用動態TLS

如果DLL也使用TLS,那麼當它用DLL_PROCESS_ATTACH標誌呼叫DllMain函式的時候,必然也呼叫了TlsAlloc;當它用DLL_PROCESS_DETACH標誌呼叫DllMain函式的時候,當然也呼叫了TlsFree。對於TlsSetValue和TlsGetValue也可能在DLL已經實現的函式中被呼叫。

使用TLS的方法之一是當應用程式需要的時候新增給應用程式。如下面的程式碼所示:

DWORD g_dwTlsIndex; // Assume that this is initialized 
									// with the result of a call to TlsAlloc.
// ...
void MyFunction(PSOMESTRUCT pSomeStruct) {
  if (pSomeStrunct != NULL) {
    // The caller is priming this function
    // See if we already allocated space to save the data.
    if(TlsGetValue(g_dwTlsIndex) == NULL) {
      // Space was never allocated. This is the first 
      // time this function has ever been called by this thread.
      TlsSetValue(g_dwTlsIndex, 
                  HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct)));
    }
    // Memory already exists for the data;
    // save the newly passed values.
    memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct));
  } else {
    // The call already primed the function. Now it 
    // wants to do something with the saved data.
    
    // Get the address of the saved data.
    pSomeStruct = (PSOMESTRUCT) TlsGetValue(g_dwTlsIndex);
    
    // The saved data is pointed to by pSomeStruct; use it.
    // ...
  }
}

如果上面的程式沒有沒呼叫過,也就不必為執行緒分配記憶體塊了。

事實上每個DLL可能或多或少地用到TLS,為此最好的做法是在TLS中儲存記憶體塊的指標,而不是把整個記憶體卡放進去。Windows2000以後可以設定1000多個TLS,像上面的方式節省地使用仍然是比較好的策略。

關於TlsAlloc函式,還有一些值得一提的功能:

DWORD dwTlsIndex;
PVOID pvSomeValue;
// ...
dwTlsIndex = TlsAlloc();
TlsSetValue(dwTlsIndex, (PVOID) 12345);
TlsFree(dwTlsIndex);
// ...
// Assume that the dwTlsIndex value returned from
// this call to TlsAlloc is identical to the index 
// return by the earlier call to TlsAlloc.
dwTlsIndex = TlsAlloc();
pvSomeValue = TlsGetValue(dwTlsIndex);

第十一行的的函式的返回值已經不是12345,而是0。因為TlsAlloc在返回之前,要遍歷程序中的每個執行緒,在每個執行緒的陣列中的新分配索引處放入0。這是因為也許已經有新的DLL被執行緒載入到地址空間中,那麼執行緒如果呼叫TlsAlloc,獲得的陣列的首地址上的元素就是原來的12345了,執行緒也許會因為這個而無法執行。

21.2 靜態TLS

與動態TLS相同,靜態的TLS也可以將資料與執行緒聯絡起來。不過相比之下靜態TLS要簡單得多,不必呼叫任何函式就可以做到。例如你想將起始時間與應用程式建立的每個執行緒聯絡起來,只需要將其實時間變數宣告為下面的形式:

__declspec(thread) DWORD gt_dwStartTime = 0;

__declspec(thread)這個字首是Microsoft新增給Visual C++編譯器的一個修飾符,它告訴編譯器被修飾的變數將放入可執行檔案或DLL檔案中它自己的節中。__declspec(thread)修飾的變數必須是一個靜態的或者全域性的變數。當上面的程式碼被編譯之後,編譯器會將所有的TLS變數放入程式自己的節中,這個節就是.tls。

為了使得靜態TLS能夠執行,作業系統將搜尋你的可執行檔案中的.tls節,並分配一塊足夠大的記憶體來存放它。而每當你的應用程式使用靜態TLS中的其中一個變數之時,必須將其轉化為應用程式中的記憶體中的位置,因此編譯器會有一些程式碼來做這些工作。所以TLS的數目也是對應用程式的效能的考驗。在x86CPU上,將為每次引用的靜態TLS變數生成3個輔助機器指令。

新執行緒都會有一塊新的記憶體塊,以存放執行緒的靜態TLS變數。每個執行緒也都對自己的靜態TLS變數有訪問許可權,不能訪問屬於其他執行緒的TLS變數。

如果應用程式和DLL都需要使用靜態的TLS變數,那麼系統在載入應用程式的時候首先會確定tls節的大小,並將DLL中的可能存在的tls節的大小相加。然後當在程序中建立執行緒的時候,系統自動分配足夠大的記憶體塊來存放所有TLS變數。

當系統呼叫LoadLibrary的時候,系統會產看程序中已經存在的執行緒,並擴大它們的記憶體塊,以便適應新的DLL對記憶體的需求。而如果呼叫FreeLibrary釋放包含靜態TLS變數的DLL的時候,被擴大的記憶體也將被壓縮。

這樣的任務對於系統來說,有些重了。雖然系統允許包含靜態TLS變數的庫在執行時顯示載入,但是TLS資料不會被初始化。如果這些資料被訪問,有可能出現訪問違規。相比之下,使用動態的TLS就不會有這樣的問題,可以在執行時釋放,很靈活不會產生問題。