淺析Windows操作系統中的線程局部存儲(TLS)機制


多線程是編程中比較容易出問題的一塊兒,究其原因,是因為多線程程序往往違背了高級語言屏蔽系統底層細節的設想,而需要程序員對於操作系統的調用機制有深入了解。會用高級語言寫算法程序->編寫多線程程序可能是一個比較困難的跨越。當然,對於多線程程序來說,即使不掌握操作系統的細節,如果學過一些操作系統的通用原理,可能也是可以勉強寫出程序來的,但是對程序的控制的和理解可能就不那么過硬。假如多線程程序又包含了多模塊(DLL動態加載),則如果不能理解內部的機制,寫出的程序可能就是一場災難。

在應對多模塊對DLL的調用時,Windows提供了TLS(Thread Local Storage,線程局部存儲機制)。雖然在不調用DLL的應用程序中依然可以使用TLS,但是操作系統設計者並不建議過多使用TLS,而在普通應用程序中,應盡量避免TLS的使用。但對於DLL來說,TLS是一種對靜態和全局變量的替代。以下內容大部分援引或簡述自《Windows核心編程》(Windows Via C/C++),更多實現細節參考了MATT  PIETREK的《Windows 95 System Programming Secrets》,也會偶有自己的評論,當然更加詳盡的關於Windows多線程編程的內容,可以參閱操作系統原理、Windows內核講解的書籍。

線程局部存儲提供一種將數據綁定到特定線程中的機制。通過這種機制,可以將一些原來被線程共享的全局變量(由於線程在操作系統中沒有自己的內存空間而與同一進程中的其他線程共享空間,所以對於線程來說,全局變量不是線程私有的)轉化為線程私有,進而讓某些由於編寫時間比較早未考慮多線程並發又使用了過多全局變量的程序有了可以支持多線程的方式。當然,TLS不只是針對上述情況的。

比如,早期微軟的C語言運行時庫就是為單線程編寫的,里面的實現用到了很多全局和靜態變量。而在后期維護的過程中,為了支持多線程,就大量使用了TLS。

關於全局變量的使用,Windows核心編程中作者曾這樣寫到:

"In my own software projects, I avoid global variables as much as possible. If your application uses global and static variables, I strongly suggest that you examine each variable and investigate the possibilities for changing it to a stack-based variable. This effort can save you an enormous amount of time if you decide to add threads to your application, and even single-threaded applications can benefit."

大意就是應盡量避免使用全局變量,如果使用到了,應盡量將其改變為棧中存儲的變量。這樣的努力可以在你試圖加入多線程時節省你很多時間,即使單線程程序也會因此而獲益。

TLS分兩種:靜態的和動態的。他們可以同時使用在普通應用程序或DLL中。但其對DLL來說意義更大:因為DLL並不知道調用程序的內部結構。在普通應用程序中一個線程應盡量使用局部變量。

動態TLS:

上圖顯示了在操作系統的內存空間中,每個線程動態TLS的分配情況圖。每個線程的局部變量的分配情況對應數組中的一個bit,值為FREE或者INUSE(可能分別對應0和1)。它對應相應下標(index)的動態存儲結構(slot)的分配情況。TLS_MINIMUM_AVAILABLE表示系統能承載slot的最大數目,在Windows系統中為64。除了bit位標志數組來標記slot的存儲情況,還有實際存儲slot的PVOID(應該是空指針)型數組,其成員個數與bit數組相同,且成員一一對應。關於bit flag數組和slot數組的具體實現細節《windows核心編程》並沒有過多提到,我參考了下MATT  PIETREK的《Windows 95 System Programming Secrets》,內容援引如下:


THE WINDOWS 95 PROCESS DATABASE (PDB)
In Windows 95, each process database is a block of memory allocated from
the KERNEL32 shared memory heap. KERNEL32 often uses the acronym
PDB instead of the longer term "process database." Unfortunately, in Win16,
PDB is a synonym for the DOS PSP that all programs have. Is this confusing?
Yes! For the purposes of this chapter, I'll use PDB in the KERNEL32 sense of
the term. Each PDB is considered to be a KERNEL32 object as evidenced by
the value 5 (K32OBJ_PROCESS) in the first DWORD of the structure. The
PROCDB.H file from the WIN32WLK program gives a C-style view of the
PDB structure.

....
88h  DWORD  tlsInUseBits1
These 32 bits represent the status of the lowest 32 TLS (Thread Local Storage)
indexes. If a bit is set, the TLS index is in use. Each successive TLS index is
represented by successively greater bit values; for example:
TLSindex:0 = 0x00000001
TLSindex:l = 0x00000002
TLSindex:2 = 0x00000004
Thread local storage is discussed in detail in the "Thread Local Storage"
section later in this chapter.
8Ch  DWORD  tlsInUseBits2
This DWORD represents the status of TLS indices 32 through 63. See the
previous field description (88h) for more information.

...

THE THREAD DATABASE
The thread database is a KERNEL32 object (type K32OBJ_THREAD) that's
allocated from the KERNEL32 shared heap. Like process databases, the
thread databases aren't directly linked together in a linked-list fashion. The
THREADB.H file from the WIN32WLK sources has a C-style structure defi-
nition for a thread database.

...
3Ch  PDWORD  pTLSArray
This pointer points to the thread's TLS array. The entries in this array are
used by the TlsSetValue family of functions. TLS is described later in this
chapter. The actual memory for the TLS array comes a bit later in the
thread database.
...
98h  DWORD  TLSArray[64]
The TLSArray field is an array of 64 DWORDs. Each DWORD holds the
value that TLSGetValue returns for a given TLS ID. For instance, the first
DWORD in the array is returned by TLSGetValue(0). The second DWORD
is returned by TLSGetValue(1), and so on. TLS is described in a subsequent
section of this chapter.
...

原文有些晦澀,因為涉及了大量的實現細節,如Windows內核的實現和在內存中的存放。內容大約是bit flag數組的前32位和后32位分別存儲在一個DWORD類型變量中,這兩個數組存儲在進程數據庫(PDB)中。而PVOID型數據的基址和實際的數據則存儲在線程數據庫中。關於線程數據庫和進程數據庫以及Windows系統的其他細節,可以進一步閱讀MATT  PIETREK的大作,我這里就不班門弄斧了。。。

TLS訪問實際數據主要通過PVOID數組中的DWORD類型的成員。這個成員存儲的一般應該是線程私有變量的地址,PVOID應該是類似void指針的一種數據類型。

講完了動態TLS的機制,剩下的就是操作系統提供給TLS的接口了。主要函數有以下四個:

DWORD TlsAlloc();

BOOL TlsSetValue( DWORD dwTlsIndex, PVOID pvTlsValue);

PVOID TlsGetValue(DWORD dwTlsIndex);

BOOL TlsFree(DWORD dwTlsIndex);

功能分別為獲取一個Tls的索引,向slot數組中設置一個PVOID的指針,獲取一個PVOID指針以及釋放一個相應索引的Tls。函數接口並不難理解,在TlsAlloc中會將同進程中所有線程相應索引的PVOID數組全部設為0,其目的是為了防止訪問到之前FREE調的臟數據。

關於索引的Tls存儲位置,《Windows核心編程》描述如下:

“A DLL (or an application) usually saves the index(就指TLS索引) in a global variable. This is one of those times when a global variable is actually the better choice because the value is used on a perprocess basis rather than a per-thread basis.”

很清楚,作者推薦將Tls索引存儲到進程的全局數據段中,這也是為何說Tls其實就是針對全局變量的多線程化的。

關於動態Tls機制,可以理解為操作系統為每一個線程提供了一個同步的內存空間,這些內存空間的結構(Tls的索引)相同,所指數據的含義(或用處)相同,但實際數據不同。由於索引是統一的,所以這個索引就存儲為全局變量。

靜態TLS

靜態TLS的用法比較簡單。只需要在全局或靜態變量的聲明前加入__declspec(thread)即可。

如:__declspec(thread) DWORD gt_dwStartTime = 0;

__declspec(thread)聲明的局部變量(棧中生存)是沒有意義的。

聲明了__declspec(thread)的變量,會為每一個線程創建一個單獨的拷貝,而對__declspec(thread)類型的變量的訪問,編譯器會做單獨處理。


以上簡略介紹了Windows操作系統中的TLS線程局部存儲機制,主要參考了一些經典書籍。關於更詳盡和更深入的細節,或者你想在程序中使用這些功能,還請參閱以上提到的參考書目。


參考書目:

MATT  PIETREK 《Windows 95 System Programming Secrets》

Jeffrey Richter , Christophe Nasarre 《Windows via C/C++, Fifth Edition》

 

另:

關於TLS的一個應用就是MFC中的線程模塊狀態的管理。以下帖子是一個簡要介紹MFC TLS的帖子:

 

原文:http://www.cnblogs.com/moonz-wu/archive/2008/05/08/1189021.html

線程局部存儲TLS

    Windows操作系統提供了Process/Thread的程序模型,其中Process是資源的分配對象
,掌握了程序所擁有的資源,而Thread則代表了程序的運行,是操作系統調度的對象。需
要注意,操作系統中,這兩種東西都是一種KERNEL32對象。分別由Process DataBase和Th
read DataBase來表示。具體可以參考Matt Petrik的Windows 95 Programing Secret

    Thread Local Storage是一個實現Thread的全局數據的機制,並且這些數據僅僅在這
個Thread中可見,因為這些數據保存在該Thread的Thread DataBase中:在每一個Thread
DataBase中都定義了一個64元的DWORD數組用來保存這些數據。同時操作系統也提供了相應
的函數來完成對這些數據的操作,如:TlsAlloc,TlsFree,TlsSetValue,TlsGetValue。

    在MFC中,也提供了TLS功能,為此MFC設計了一系列的類和程序來完成這個任務。具體
的程序在afxtls.cpp和afxtls_.h中。
涉及到的主要的類有:

    class CTypedSimpleList : public CSimpleList
    struct CThreadData : public CNoTrackObject
    struct CSlotData
    class CThreadSlotData
    class CThreadLocal : public CThreadLocalObject

    其中CThreadSlotData是封裝TLS的最重要的類,CTypedSimpleList,CSlotData,CTh
readDAta都是為了封裝TLS而設計的只具有輔助功能的類。CThreadLocal是更高層的封裝。

    首先讓我們來對其數據封裝方式進行分析,重要的類的定義及其分析如下所示:(為簡
單起見,只列出數據成員而不再列出函數成員)

定義:

    class CThreadSlotData
    {
        public:
        DWORD m_tlsIndex;
        int m_nAlloc;   
        int m_nRover;
        int m_nMax;   
        CSlotData* m_pSlotData;
        CTypedSimpleList<CThreadData*> m_list;
        CRITICAL_SECTION m_sect;
    };

分析:

    在afxtls.cpp中定義了一個CThreadSlotData類的全局變量:_afxThreadData。在CTh
readLocal的成員函數中大量使用了這個全局變量來訪問TLS功能。

    DWORD m_tlsIndex

    用來保存TLS數據的索引,也就是在Thread DataBase中64元數組中的偏移量,這個數據在
CThreadSlotData類的構造函數中初始化。

    int m_nAlloc
    int m_nRover
    int m_nMax

    這三個變量用來分配slot和記錄相關狀態,比如m_nAlloc用來保存當前已經分配的slot的
個數。線程為每一個TLS數據分配一個slot。

    CSlotData* m_pSlotData;

    用來記錄已經分配的每一個slot的狀態:已經使用或是尚未使用。

    CTypedSimpleList<CThreadData*> m_list;

    CThreadSlotData為每一個Thread實現一個並且只實現一個CThreadData對象,並且用鏈表
類對象m_list來管理它們。實際上,真正被保存到Thread DataBase中去的是這個CThread
Data對象的指針,而程序員要保存的TLS數據被保存到這個CThreadData對象的pData成員指
向的動態數組中。所有Thread的CThreadData對象通過CThreadData對象的pNext成員連成鏈
表,並由CTypedSimpleList<CThreadData*> m_list管理。

    CRITICAL_SECTION m_sect;

    由於所有Thread的TLS操作都要靠訪問_afxThreadData來實現,這樣就產成了多線程同步的
問題,m_sect就是用來進行線程同步的變量。保證每次只有一個Thread在訪問_afxThread
Data中的成員變量。

定義:

    struct CThreadData : public CNoTrackObject
    {
        CThreadData* pNext; // required to be member of CSimpleList
        int nCount;         // current size of pData
        LPVOID* pData;      // actual thread local data (indexed by nSlot)
    };

分析:

    CThreadData用來輔助CThreadSlotData來完成TLS功能。每一個Thread的TLS數據都要
靠一個CThreadData對象來管理和保存。

    CThreadData* pNext

    在CThreadSlotData中,CThreadData由一個鏈表來管理,pNext用來把各個Thread的CThre
adData對象連成鏈表。

    int nCount

    指出用於保存TLS數據指針的動態數組的長度。

    LPVOID* pData

    在CThreadData保存的實際上是各個TLS數據的指針,為此定義了一個指針數組,nCount用
來指示數組長度,pData用來指出數組的基地址。

定義:

    struct CSlotData
    {
        DWORD dwFlags;      // slot flags (allocated/not allocated)
        HINSTANCE hInst;    // module which owns this slot
    };

分析:

    CSlotData用來輔助CThreadSlotData來完成TLS功能。每一個Thread的TLS數據都要靠
一個CThreadData對象來保存,具體實現是把TLS數據的指針保存在CThreadData對象的動態
指針數組中(基地址由pData指出)。而這個數組中每一個成員的使用狀況則由一個與之長度
相同的CSlotData數組來表示,具體由DWORD dwFlags來表明。

    從上面的分析不難發現,MFC中TLS功能的封裝是這樣的,所有Thread的TLS數據指針都
保存在一個動態的指針數組中,而該數組的基地址由一個CThreadData對象的 pData指出。
同時,保存在Thread DataBase中的是這個CThreadData對象的指針,而不是TLS數據的指針
,並且其索引值均相同,都為CThreadSlotData類中的m_tlsIndex成員。而且,在CThread
SlotData中提供了一個鏈表來管理所有Thread的CThreadData對象。這樣CThreadSlotData
類就能訪問所有的Thread的TLS數據。見圖tls.bmp。(為了方便,我把圖放到了簽名檔中了
,就在下面)

    下面來進一步說明如何使用TLS功能。

    為了方便TLS的使用,MFC設計了CThreadLocal類。它是一個模板類,具體的定義如下:
    
    template<class TYPE>
    class CThreadLocal : public CThreadLocalObject
    {
    // Attributes
    public:
        AFX_INLINE TYPE* GetData()
        {
            TYPE* pData = (TYPE*)CThreadLocalObject::GetData(&CreateObject);
            ASSERT(pData != NULL);
            return pData;
        }
        AFX_INLINE TYPE* GetDataNA()
        {
            TYPE* pData = (TYPE*)CThreadLocalObject::GetDataNA();
            return pData;
        }
        AFX_INLINE operator TYPE*()
        { return GetData(); }
        AFX_INLINE TYPE* operator->()
        { return GetData(); }

    // Implementation
    public:
        static CNoTrackObject* AFXAPI CreateObject()
        { return new TYPE; }
    };

    在使用CThreadLocal時,只要用CThreadLocal<ClassType> name;即可構造一個類型為
ClassType的TLS數據,注意ClassType必須以CNoTrackObject為基類。實際上上述聲明定義
了一個名稱為name的CThreadLocal對象,但是通過這個CThreadLocal對象,即可生成並訪
問類型為ClassType的TLS數據。


關於MFC的模塊狀態管理,可以參閱李久進的《MFC深入淺出》第九章,MFC的狀態,鏈接:http://www.vczx.com/tutorial/mfc/mfc9.php。

更深入的了解,可以閱讀MFC源碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM