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

來源:互聯網
上載者:User

多線程是編程中比較容易出問題的一塊兒,究其原因,是因為多線程程式往往違背了進階語言屏蔽系統底層細節的設想,而需要程式員對於作業系統的調用機制有深入瞭解。會用進階語言寫演算法程式->編寫多線程程式可能是一個比較困難的跨越。當然,對於多線程程式來說,即使不掌握作業系統的細節,如果學過一些作業系統的通用原理,可能也是可以勉強寫出程式來的,但是對程式的控制的和理解可能就不那麼過硬。假如多線程程式又包含了多模組(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(應該是null 指標)型數組,其成員個數與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源碼。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.