天天看點

線程本地存儲TLS(Thread Local Storage)的原理和實作——分類和原理

本文為線程本地存儲TLS系列之分類和原理。

一、TLS簡述和分類

我們知道在一個程序中,所有線程是共享同一個位址空間的。是以,如果一個變量是全局的或者是靜态的,那麼所有線程通路的是同一份,如果某一個線程對其進行了修改,也就會影響到其他所有的線程。不過我們可能并不希望這樣,是以更多的推薦用基于堆棧的自動變量或函數參數來通路資料,因為基于堆棧的變量總是和特定的線程相聯系的。

不過如果某些時候(比如可能是特定設計的dll),我們就是需要依賴全局變量或者靜态變量,那有沒有辦法保證在多線程程式中能通路而不互相影響呢?答案是有的。作業系統幫我們提供了這個功能——TLS線程本地存儲。TLS的作用是能将資料和執行的特定的線程聯系起來。

實作TLS有兩種方法:靜态TLS和動态TLS。以下我們将分别說明這兩類TLS。

二、靜态TLS

1、使用靜态TLS

之是以先講靜态TLS,是因為他在代碼中使用時非常簡單,我們隻需寫類似如下這一句:

__declspec(thread) DWORD myTLSData=0;

我們就為本程式中的每一個線程建立了一個獨立的DWORD資料。

__declspec(thread)的字首是Microsoft添加給Visual C++編譯器的一個修改符。它告訴編譯器,對應的變量應該放入可執行檔案或DLL檔案中它的自己的節中。__declspec(thread)後面的變量必須聲明為函數中(或函數外)的一個全局變量或靜态變量。不能聲明一個類型為__declspec(thread)的局部變量,你想,因為局部變量總是與特定的線程相聯系的,如果再加上這個聲明是代表什麼意思?

2、靜态TLS原理

靜态TLS的使用是如此簡單,那麼當我們寫了如上代碼以後,作業系統和編譯器是怎麼處理的呢?

首先,在編譯器對程式進行編譯時,它會将所有聲明的TLS變量放入它們自己的節,這個節的名字是.tls。而後連結程式将來自所有對象子產品的所有.tls節組合起來,形成結果的可執行檔案或DLL檔案中的一個大的完整的.tls節。

然後,為了使含有靜态TLS的程式能夠運作,作業系統必須參與其操作。當TLS應用程式加載到記憶體中時,系統要尋找可執行檔案中的.tls節,并且動态地配置設定一個足夠大的記憶體塊,以便存放所有的靜态TLS變量。應用程式中的代碼每次引用其中的一個變量時,就要轉換為已配置設定記憶體塊中包含的一個記憶體位置。是以,編譯器必須生成一些輔助代碼來引用該靜态TLS變量,這将使你的應用程式變得比較大而且運作的速度比較慢。在x86 CPU上,将為每次引用的靜态TLS變量生成3個輔助機器指令。如果在程序中建立了另一個線程,那麼系統就要将它捕獲并且自動配置設定另一個記憶體塊,以便存放新線程的靜态TLS變量。新線程隻擁有對它自己的靜态TLS變量的通路權,不能通路屬于其他線程的TLS變量。

以上是包含靜态TLS變量的可執行檔案如何運作的情況。我們再來看看DLL的情況:

a、隐式連結包含靜态TLS變量的DLL

如果應用程式使用了靜态TLS變量,并且隐式連結包含靜态TLS變量的DLL時,當系統加載該應用程式時,它首先要确定應用程式的.tls節的大小,并将這個值與應用程式連結的DLL中的所有.tls節的大小相加。當在你的程序中建立線程時,系統自動配置設定足夠大的記憶體塊來存放所有應用程式聲明的和所有隐含連結的DLL包含的TLS變量。

b、顯式連結包含靜态TLS變量的DLL

考慮一下,當我們的應用程式通過調用LoadLibrary,以便顯式連結到包含靜态TLS變量的DLL時,會發生什麼情況呢?系統必須檢視該程序中已經存在的所有線程,并擴大它們的TLS記憶體塊,以便适應新DLL對記憶體的需求。另外,如果調用FreeLibrary來釋放包含靜态TLS變量的DLL,那麼與程序中的每個線程相關的的TLS記憶體塊又都應該被壓縮。

對于作業系統來說,這樣的管理任務太重了。是以,雖然系統允許包含靜态TLS變量的庫在運作期進行顯式加載,但是其包含TLS資料卻沒有進行相應的初始化。如果試圖通路這些資料,就可能導緻通路違規!

是以,請記住:如果某個DLL包含靜态TLS資料,請不要對這個DLL采用顯式連結的方式,否則可能會出錯!

三、動态TLS

1、使用動态TLS

動态TLS在程式實作中比靜态TLS要稍微麻煩一些,需要通過一組函數來實作:

DWORD TlsAlloc();//傳回TLS數組可用位置的索引

BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue); //将調用線程的TLS數組索引dwTlsIndex處設為值lpTlsValue

LPVOID TlsGetValue(DWORD dwTlsIndex); //傳回調用線程的TLS數組dwTlsIndex索引處的值

BOOL TlsFree(DWORD dwTlsIndex); //釋放所有線程的TLS數組位置索引dwTlsIndex,将該位置标記為未使用。

有了以上四個函數,我們可以發現使用動态TLS其實還是很容易很友善的。

2、動态TLS原理

讓我們看看windows用來管理TLS的内部資料結構:

線程本地存儲TLS(Thread Local Storage)的原理和實作——分類和原理

線程本地存儲器的位标志顯示了該程序中所有運作的線程正在使用的一組标志。每個标志均可設定為FREE或者INUSE,表示TLS插槽(slot)是否正在使用。Microsoft保證至少TLS_MINIMUM_AVAILABLE位标志是可供使用的。另外,TLS_MINIMUM_AVAILABLE在WinNT.h中被定義為64。Windows2000将這個标志數組擴充為允許有1000個以上的TLS插槽。

而每一個線程擁有一個自己獨立的TLS slot數組,用于存儲TLS資料。

為了使用動态TLS,我們首先調用TlsAlloc()來指令系統對程序的位标志進行掃描,找到一個可用的位置,并傳回該索引;如果找不到,就傳回TLS_OUT_OF_INDEXES。事實上,除此之外,TlsAlloc函數還會自動清空所有線程的TLS數組的對應索引的值。這避免以前遺留的值可能引起的問題。

然後,我們就可以調用TlsSetValue函數将對應的索引位儲存一個特定的值,可以調用TlsGetValue()來傳回該索引位的值。注意,這兩個函數并不執行任何測試和錯誤檢查,我們必須要保證索引是通過TlsAlloc正确配置設定的。

當所有線程都不需要保留TLS數組某個索引位的時候,應該調用TlsFree。該函數告知系統将程序的位标志數組的index位置為FREE狀态。如果運作成功,函數傳回TRUE。注意,如果試圖釋放一個沒有配置設定的索引位,将産生一個錯誤。

動态TLS的使用相對靜态TLS稍微麻煩一點,但是無論是将其用在可執行檔案中還是DLL中,都還是很簡單的。而且當用在DLL中時,沒有由于DLL連結方式而可能産生的問題,是以,如果要在DLL中用TLS,又不能保證客戶始終采用隐式連結方式,那麼請采用動态TLS的實作。

本文轉自莫水千流部落格園部落格,原文連結:http://www.cnblogs.com/zhoug2020/p/6497709.html,如需轉載請自行聯系原作者