Windows核心編程【21】小結

第21章 線程局部存儲區


有時將數據與一個對象的實例關聯起來是有幫助的。我們可以使用線程局部存儲區(Thread Local Storage,簡稱TLS)來將數據與一個正在執行的指定線程關聯起來。例如,可以將創建線程的時間與線程關聯起來,然後當線程終止的時候,就可以確定線程運行的時間長度。


(TLS可以實現“對各線程有不同意義的全局變量”)


C/C++運行庫使用了TLS。由於C/C++運行庫是在多線程應用程序出現的許多年前設計的,很多都是爲單線程應用程序設計的。重要數據保存在靜態變量中,這在多線程同時調用的情況下,容易導致靜態變量的覆蓋,從而出錯。爲解決這個問題,C/C++運行庫使用了TLS,爲每個線程分配獨立的字符串指針。


作爲開發人員,應該最大限度地減少對全局變量或靜態變量的使用,並更多地依賴於自動變量(棧上的變量)和通過函數參數傳入的數據。這是好事,因爲棧上的變量始終都是與某個特定的線程相關聯的。


TLS在創建DLL的時候更加有用,因爲DLL通常並不知道它們被鏈接到的應用程序的結構是什麼樣的。


一、動態TLS


圖21-1用來管理TLS的內部數據結構



系統中的每個進程都有一組正在使用標誌(in-use flag),如上所示。每個標誌可以被設爲FREE或INUSE,表示該TLS元素是否正在使用。MS保證至少有TLS_MINIMUM_AVALIABLE(在WinNT.h中定義爲64,系統會在需要的時候分配更多的TLS元素,最多可達1000多個。)個位表示可供使用。


先調用TlsAlloc,讓系統對進程中的位標識進程檢索並找到一個FREE標誌。然後系統會將該標誌從FREE改爲INUSE並讓TlsAlloc返回該標誌在位數組中的索引。一個DLL(或應用程序)通常將這個索引保存在一個全局變量中。(因爲這個值會在整個進程範圍內使用,而不是在線程範圍內使用)如果無法找到一個FREE標誌,返回TLS_OUT_OF_INDEXES(在WinBase.h中定義爲0xfffffff)。


當系統創建一個線程的時候,會分配TLS_MINIMUM_AVAILABLE個PVOID值,將它們都初始化爲0,並與線程關聯起來。如上圖所示,每個線程都有自己的PVOID數組,數組中的每個PVOID可以保存任意值。


在能夠將信息保存到線程的PVOID數組之前,必須知道數組中的哪個索引可供使用——正是前面調用TlsAlloc的目的。爲了把一個值放到線程的數組中,應該調用TlsSetValue函數。http://msdn.microsoft.com/en-us/library/windows/desktop/ms686818(v=vs.85).aspx
BOOL WINAPI TlsSetValue(
  __in      DWORD dwTlsIndex,
  __in_opt  LPVOID lpTlsValue
);


這個函數把pvTlsValue參數所標識的一個PVOID值放到線程的數組中,參數dwTlsIndex標識一個索引值,表示在數組中的具體位置(前面通過TlsAlloc調用返回的值)。調用成功則返回TRUE。


相應的TlsGetValue獲得值。64個TLS不能說是用之不竭的。應用程序可能會加載很多DLL,每個DL要幾個就沒了。一般情況下,都是動態申請一個結構(把數據封裝起來),然後TLS存放指針。


不需要一個已經預定的TLS元素事,使用TlsFree。進程會將進程內的位標識數組中對應的INUSE標誌重新設回FREE,此外,還會將所有線程中該元素的內存設爲0。試圖釋放一個尚未分配的TLS元素將導致錯誤。


TlsAlloc會在返回之前,根據新分配的索引,在每個線程的數組中把對應的元素設爲0。


(TlsAlloc是向進程申請一個位標識,然後可以使用該位標識來“私有化”各個線程。線程不能直接讀取其餘線程的數組。)


二、靜態TLS


靜態TLS使用的時候不必在代碼中調用任何函數,因而更容易使用。


__declspec(thread)前綴是MS爲VC編譯器增加的一個修飾符,告訴編譯器在可執行文件或DLL文件中,把對應的變量放到它自己的段中。__declspec(thread)後面的變量必須被聲明爲全局變量或靜態變量(既可以在函數內,也可以在函數外)。


當編譯器對剩下進行編譯的時候,會將所有TLS變量放到它們自己的段中,這個段名爲.tls。鏈接器會將所有對象模塊中的.tls段合併成爲一個大的.tls段,並將它保存到生成的可執行文件或DLL文件中。


爲了讓TLS能夠正常工作,OS也必須參與進來。當系統將應用程序加載到內存的時候,會查看可執行文件中的.tls段,並分配一塊足夠大的內存來保存所有的靜態TLS變量。每當應用程序中的代碼引用到這些變量之一時,相應的引用會被解析到剛分配的這塊內存中的一個位置。因此,編譯器必鬚生成額外的代碼來引用靜態TLS變量,這使得應用程序不僅變得更大,而且只需起來也更慢。在x86CPU上,每次引用一個靜態TLS變量會生成三條額外的機器指令。


如果進程創建了另一個線程,那麼系統會獲知這一情況並自動分配另一塊內存來保存新線程的靜態TLS變量。新線程只能訪問自己的靜態TLS變量,無法訪問任何其他線程的TLS變量。


如果應用程序調用LoadLibrary來鏈接一個DLL,而且該DLL保護了靜態TLS變量,這時會發生什麼情況。爲了給新DLL提供它需要的額外TLS內存,系統必須查看進程中所有已有的線程,並擴大它們的TLS內存塊。另外,如果應用程序調用FreeLibrary來釋放一個DLL,而且該DLL包含了靜態TLS變量,那麼與進程中的每個線程相關聯的內存塊也應該相應地縮減。好消息是,Vista對此提供了完全支持。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章