Win32多線程編程 — 線程局部存儲

預留內存攜帶附加信息的設計

有時候,將數據與一個對象的實例關聯起來是很有幫助的。這種設計要求預留一定的內存,一倍特定附加數據的存儲。

通過調用SetWindowWordSetWindowLong函數將數據與一個指定的窗口關聯起來,數據保存在窗口附加內存塊中。窗口內存塊即是一種窗口對象(HWND)的附加數據(window extra bytes),參考WNDCLASS.cbWndExtra字段(Specifies the number of extra bytes to allocate following the window instance.)。

這種預留附加的設計,在MFC中處處可見。對於下拉選擇列表(CComboBox)、下拉列表框、列表視圖和樹控件,我們不光希望其能顯示條目內容(item text),還希望每個條目能夠攜帶附加信息,即存儲額外的關聯數據(item data),以備不時之需。這四個控件都提供了SetItemData/GetItemData接口,供用戶儲存關聯數據。存儲的數據爲DWORD值類型,可以是簡單的數值,也可以存儲指針。

 

線程消息隊列和_ptiddata

我們在編寫第一個SDK窗口程序時,就接觸到了消息這一重要概念。實際上,消息隊列是一種線程私有數據,每一個Windows程序的UI(CUI/GUI)線程都維持了一個消息隊列。GetMessageTranslateMessageDispatchMessage等對消息的操作都是與調用線程的消息隊列息息相關。PostThreadMessage是線程消息投遞函數,它向一個指定ID(idThread)的線程發送一條消息,然後不等處理立即返回。這個API在多線程架構程序中非常有用。PostQuitMessage是結束線程運行,相當於nExitCode作爲WM_QUIT消息參數調用PostThreadMessage。調用線程收到該消息後即ExitThread,故該函數一般用來響應WM_DESTROY消息。

儘管秉持封裝的原則,我們極力強調避免使用全局變量,但全局變量對於進程級和線程級的系統統籌管理卻是非常有用。除了消息隊列這種系統內置的線程私有數據外,Windows提供了線程局部存儲系統(TLS,Thread Local Storage),爲用戶提供了存儲與線程關聯數據的接口。前面提到的_beginthreadex中分配的_ptiddatapointer to per-thread data),即使用了TLS。_ptiddata爲Windows平臺的多線程程序中,strtokstrerrorerrno等依賴全局變量或靜態變量的CRT函數的實現提供了有效的解決方案。

 

Win32線程局部存儲系統

用於管理 TLS 的數據結構是很簡單的,Windows僅爲系統中的每一個進程維護一個位數組,再爲該進程中的每一個線程申請一個同樣長度的數組空間,如下圖所示。

    在Windbg中,可以窺探TEB中的TLS數據結構。

lkd> dt _teb

nt!_TEB

   +0x02c ThreadLocalStoragePointer : Ptr32 Void

   +0xe10 TlsSlots         : [64] Ptr32 Void

   +0xf10 TlsLinks         : _LIST_ENTRY

   +0xf94 TlsExpansionSlots : Ptr32 Ptr32 Void

 

typedef struct _TEB // 66 elements, 0xFB8 bytes (sizeof)

{

    // ……

    /*0x02C*/     VOID*        ThreadLocalStoragePointer;

    // ……

    /*0xE10*/     VOID*        TlsSlots[64];

    /*0xF10*/     struct _LIST_ENTRY TlsLinks// 2 elements, 0x8 bytes (sizeof)

    // ……

    /*0xF94*/     VOID**       TlsExpansionSlots;

    // ……

}TEB, *PTEB;

當一個線程被創建時,Windows就會在進程地址空間中爲該線程分配一個長度爲TLS_MINIMUM_AVAILABLE的數組,數組成員的值都被初始化爲 0。在內部,系統將此數組與該線程關聯起來,保證只能在該線程中訪問此數組中的數據。如上圖所示,每個線程都有它自己的數組,數組成員可以存儲任何數據。

運行在系統中的每一個進程都有上圖所示的一個位數組。位數組的成員是一個標誌,每個標誌的值被設爲FREE或INUSE,指示了此標誌對應的數組索引是否在使用中。Windows 保證至少有TLS_MINIMUM_AVAILABLE(定義在WinNT.h文件中)個標誌位可用。

動態使用TLS典型步驟如下。

(1)主線程調用TlsAlloc函數爲線程局部存儲分配索引,函數原型如下。

DWORD TlsAlloc(VOID);

TlsAlloc爲我們預訂了一個索引。如果TlsAlloc返回的索引爲3,那等於說索引3已經被我們預訂了,無論是進程中當前正在運行的線程,還是今後可能會創建的線程,都不能再使用索引3。

(2)每個線程調用TlsSetValueTlsGetValue設置或讀取線程數組中的值,這兩個函數的原型如下。

BOOL TlsSetValue(

               DWORD dwTlsIndex,  // TLS index

               LPVOID lpTlsValue  // value to store

);

 

LPVOID TlsGetValue(

                 DWORD dwTlsIndex   // TLS index

);

(3)主線程調用TlsFree釋放局部存儲索引。函數的惟一參數是TlsAlloc返回的索引。

BOOL TlsFree(

            DWORD dwTlsIndex   // TLS index

            );

 

MFC中的線程局部存儲

如果你需要大量的數據貫穿一個線程,普通的TLS索引一個值就會變得不實用,Windows的TLS只允許用戶保存一個32位的指針。如果需要用戶保存任意類型的數據(包含整個類)。這個任意大小的數據所佔的內存通常是在進程的堆中分配,所以當用戶釋放全局索引時,系統必須將每個線程內此數據佔用的內存釋放掉,這就要求系統把爲各線程分配的內存都記錄下來。較好的方法是將各個私有數據的首地址用一個鏈表連在一起,釋放全局索引時只要遍歷此鏈表,就可以逐個釋放線程私有數據佔用的空間了。

例如,有下面一個存放線程私有數據的數據結構。

struct CThreadData

{

    CThreadDatapNext// 指向下一個線程的CThreadData結構的指針

    LPVOID pData;       // 指向真正的線程私有數據的指針

};

指針 pData指向爲線程分配的內存的首地址,指針pNext將各線程的數據連在了一起。這實際上是一種二級指針的分槽存儲。MFC的線程局部存儲類CThreadLocal即實現二級指針的分槽存儲。

MFC框架的狀態信息也是理解的難點,包括模塊狀態AFX_MODULE_STATE線程狀態_AFX_THREAD_STATE和模塊線程狀態AFX_MODULE_THREAD_STATE。這些線程級別的全局狀態維持即使用了線程局部存儲(TLS)。參考李久進著作的《MFC深入淺出》第九章《MFC的狀態》。

由於MFC廣泛地應用了線程局部存儲,故在MFC下,使用線程必須格外小心。許多MFC對象僅在創建它們的線程內運作。一般地,具有句柄映射的任何對象都不能從其他線程訪問該對象。例如,模塊線程狀態AFX_MODULE_THREAD_STATE中的CHandleMapm_pmapHWND映射記錄了MFC線程中創建的CWnd對象實例與內核窗口句柄(HWND)之間的映射消息。內核窗口句柄是可以進程訪問級別,因此可跨線程訪問。但是試圖傳遞CWnd對象實例以期跨線程操作,往往失敗。因爲另一個引用線程並未像創建線程那樣維繫一個映射,所以當需要CWndàHWND以執行API操作時,往往找不到其所指窗口。

針對以上問題,通常優先傳送句柄,避免在線程之間傳送MFC對象。在引用線程中將其轉換爲臨時MFC對象。例如,假設線程 A創建一個CWnd對象。線程A並不將對象傳送給線程B,而將該對象的m_hWnd成員傳送給線程B。於是,線程B可以調用CWnd::FromHandle,以創建一個臨時的CWnd對象。如果線程B需要更持久的連接,就可以使用Attach方法,在窗口及其CWnd對象之間建立持久的關聯。

另外的一個常見問題是MFC對象訪存的線程安全性問題。MFC對象不會自動在不同的線程之間做出判斷。所以,如果兩個線程試圖同時訪問同一個CString類的對象,結果可能受到嚴重破壞。只有防止來自有衝突的MFC對象的線程。通常,這將需要使用前面提到的同步機制,以保證多線程數據交換的一致性。

發佈了18 篇原創文章 · 獲贊 2 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章