Win32多线程编程(5) — 线程局部存储
預(yù)留內(nèi)存攜帶附加信息的設(shè)計(jì)
有時(shí)候,將數(shù)據(jù)與一個(gè)對(duì)象的實(shí)例關(guān)聯(lián)起來(lái)是很有幫助的。這種設(shè)計(jì)要求預(yù)留一定的內(nèi)存,一倍特定附加數(shù)據(jù)的存儲(chǔ)。
通過(guò)調(diào)用SetWindowWord或SetWindowLong函數(shù)將數(shù)據(jù)與一個(gè)指定的窗口關(guān)聯(lián)起來(lái),數(shù)據(jù)保存在窗口附加內(nèi)存塊中。窗口內(nèi)存塊即是一種窗口對(duì)象(HWND)的附加數(shù)據(jù)(window extra bytes),參考WNDCLASS.cbWndExtra字段(Specifies the number of extra bytes to allocate following the window instance.)。
這種預(yù)留附加的設(shè)計(jì),在MFC中處處可見。對(duì)于下拉選擇列表(CComboBox)、下拉列表框、列表視圖和樹控件,我們不光希望其能顯示條目?jī)?nèi)容(item text),還希望每個(gè)條目能夠攜帶附加信息,即存儲(chǔ)額外的關(guān)聯(lián)數(shù)據(jù)(item data),以備不時(shí)之需。這四個(gè)控件都提供了SetItemData/GetItemData接口,供用戶儲(chǔ)存關(guān)聯(lián)數(shù)據(jù)。存儲(chǔ)的數(shù)據(jù)為DWORD值類型,可以是簡(jiǎn)單的數(shù)值,也可以存儲(chǔ)指針。
?
線程消息隊(duì)列和_ptiddata
我們?cè)诰帉懙谝粋€(gè)SDK窗口程序時(shí),就接觸到了消息這一重要概念。實(shí)際上,消息隊(duì)列是一種線程私有數(shù)據(jù),每一個(gè)Windows程序的UI(CUI/GUI)線程都維持了一個(gè)消息隊(duì)列。GetMessage、TranslateMessage、DispatchMessage等對(duì)消息的操作都是與調(diào)用線程的消息隊(duì)列息息相關(guān)。PostThreadMessage是線程消息投遞函數(shù),它向一個(gè)指定ID(idThread)的線程發(fā)送一條消息,然后不等處理立即返回。這個(gè)API在多線程架構(gòu)程序中非常有用。PostQuitMessage是結(jié)束線程運(yùn)行,相當(dāng)于nExitCode作為WM_QUIT消息參數(shù)調(diào)用PostThreadMessage。調(diào)用線程收到該消息后即ExitThread,故該函數(shù)一般用來(lái)響應(yīng)WM_DESTROY消息。
盡管秉持封裝的原則,我們極力強(qiáng)調(diào)避免使用全局變量,但全局變量對(duì)于進(jìn)程級(jí)和線程級(jí)的系統(tǒng)統(tǒng)籌管理卻是非常有用。除了消息隊(duì)列這種系統(tǒng)內(nèi)置的線程私有數(shù)據(jù)外,Windows提供了線程局部存儲(chǔ)系統(tǒng)(TLS,Thread Local Storage),為用戶提供了存儲(chǔ)與線程關(guān)聯(lián)數(shù)據(jù)的接口。前面提到的_beginthreadex中分配的_ptiddata(pointer to per-thread data),即使用了TLS。_ptiddata為Windows平臺(tái)的多線程程序中,strtok、strerror(errno)等依賴全局變量或靜態(tài)變量的CRT函數(shù)的實(shí)現(xiàn)提供了有效的解決方案。
?
Win32線程局部存儲(chǔ)系統(tǒng)
用于管理?TLS?的數(shù)據(jù)結(jié)構(gòu)是很簡(jiǎn)單的,Windows僅為系統(tǒng)中的每一個(gè)進(jìn)程維護(hù)一個(gè)位數(shù)組,再為該進(jìn)程中的每一個(gè)線程申請(qǐng)一個(gè)同樣長(zhǎng)度的數(shù)組空間,如下圖所示。
????在Windbg中,可以窺探TEB中的TLS數(shù)據(jù)結(jié)構(gòu)。
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;
當(dāng)一個(gè)線程被創(chuàng)建時(shí),Windows就會(huì)在進(jìn)程地址空間中為該線程分配一個(gè)長(zhǎng)度為TLS_MINIMUM_AVAILABLE的數(shù)組,數(shù)組成員的值都被初始化為?0。在內(nèi)部,系統(tǒng)將此數(shù)組與該線程關(guān)聯(lián)起來(lái),保證只能在該線程中訪問此數(shù)組中的數(shù)據(jù)。如上圖所示,每個(gè)線程都有它自己的數(shù)組,數(shù)組成員可以存儲(chǔ)任何數(shù)據(jù)。
運(yùn)行在系統(tǒng)中的每一個(gè)進(jìn)程都有上圖所示的一個(gè)位數(shù)組。位數(shù)組的成員是一個(gè)標(biāo)志,每個(gè)標(biāo)志的值被設(shè)為FREE或INUSE,指示了此標(biāo)志對(duì)應(yīng)的數(shù)組索引是否在使用中。Windows?保證至少有TLS_MINIMUM_AVAILABLE(定義在WinNT.h文件中)個(gè)標(biāo)志位可用。
動(dòng)態(tài)使用TLS典型步驟如下。
(1)主線程調(diào)用TlsAlloc函數(shù)為線程局部存儲(chǔ)分配索引,函數(shù)原型如下。
DWORD?TlsAlloc(VOID);
TlsAlloc為我們預(yù)訂了一個(gè)索引。如果TlsAlloc返回的索引為3,那等于說(shuō)索引3已經(jīng)被我們預(yù)訂了,無(wú)論是進(jìn)程中當(dāng)前正在運(yùn)行的線程,還是今后可能會(huì)創(chuàng)建的線程,都不能再使用索引3。
(2)每個(gè)線程調(diào)用TlsSetValue和TlsGetValue設(shè)置或讀取線程數(shù)組中的值,這兩個(gè)函數(shù)的原型如下。
BOOL?TlsSetValue(
???????????????DWORD?dwTlsIndex,??// TLS index
???????????????LPVOID?lpTlsValue??// value to store
);
?
LPVOID?TlsGetValue(
?????????????????DWORD?dwTlsIndex???// TLS index
);
(3)主線程調(diào)用TlsFree釋放局部存儲(chǔ)索引。函數(shù)的惟一參數(shù)是TlsAlloc返回的索引。
BOOL?TlsFree(
????????????DWORD?dwTlsIndex???// TLS index
????????????);
?
MFC中的線程局部存儲(chǔ)
如果你需要大量的數(shù)據(jù)貫穿一個(gè)線程,普通的TLS索引一個(gè)值就會(huì)變得不實(shí)用,Windows的TLS只允許用戶保存一個(gè)32位的指針。如果需要用戶保存任意類型的數(shù)據(jù)(包含整個(gè)類)。這個(gè)任意大小的數(shù)據(jù)所占的內(nèi)存通常是在進(jìn)程的堆中分配,所以當(dāng)用戶釋放全局索引時(shí),系統(tǒng)必須將每個(gè)線程內(nèi)此數(shù)據(jù)占用的內(nèi)存釋放掉,這就要求系統(tǒng)把為各線程分配的內(nèi)存都記錄下來(lái)。較好的方法是將各個(gè)私有數(shù)據(jù)的首地址用一個(gè)鏈表連在一起,釋放全局索引時(shí)只要遍歷此鏈表,就可以逐個(gè)釋放線程私有數(shù)據(jù)占用的空間了。
例如,有下面一個(gè)存放線程私有數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)。
struct?CThreadData
{
????CThreadData*?pNext;?//?指向下一個(gè)線程的CThreadData結(jié)構(gòu)的指針
????LPVOID?pData;???????//?指向真正的線程私有數(shù)據(jù)的指針
};
指針?pData指向?yàn)榫€程分配的內(nèi)存的首地址,指針pNext將各線程的數(shù)據(jù)連在了一起。這實(shí)際上是一種二級(jí)指針的分槽存儲(chǔ)。MFC的線程局部存儲(chǔ)類CThreadLocal即實(shí)現(xiàn)了二級(jí)指針的分槽存儲(chǔ)。
MFC框架的狀態(tài)信息也是理解的難點(diǎn),包括模塊狀態(tài)AFX_MODULE_STATE、線程狀態(tài)_AFX_THREAD_STATE和模塊線程狀態(tài)AFX_MODULE_THREAD_STATE。這些線程級(jí)別的全局狀態(tài)維持即使用了線程局部存儲(chǔ)(TLS)。參考李久進(jìn)著作的《MFC深入淺出》第九章《MFC的狀態(tài)》。
由于MFC廣泛地應(yīng)用了線程局部存儲(chǔ),故在MFC下,使用線程必須格外小心。許多MFC對(duì)象僅在創(chuàng)建它們的線程內(nèi)運(yùn)作。一般地,具有句柄映射的任何對(duì)象都不能從其他線程訪問該對(duì)象。例如,模塊線程狀態(tài)AFX_MODULE_THREAD_STATE中的CHandleMap*?m_pmapHWND映射記錄了MFC線程中創(chuàng)建的CWnd對(duì)象實(shí)例與內(nèi)核窗口句柄(HWND)之間的映射消息。內(nèi)核窗口句柄是可以進(jìn)程訪問級(jí)別,因此可跨線程訪問。但是試圖傳遞CWnd對(duì)象實(shí)例以期跨線程操作,往往失敗。因?yàn)榱硪粋€(gè)引用線程并未像創(chuàng)建線程那樣維系一個(gè)映射,所以當(dāng)需要CWndàHWND以執(zhí)行API操作時(shí),往往找不到其所指窗口。
針對(duì)以上問題,通常優(yōu)先傳送句柄,避免在線程之間傳送MFC對(duì)象。在引用線程中將其轉(zhuǎn)換為臨時(shí)MFC對(duì)象。例如,假設(shè)線程?A創(chuàng)建一個(gè)CWnd對(duì)象。線程A并不將對(duì)象傳送給線程B,而將該對(duì)象的m_hWnd成員傳送給線程B。于是,線程B可以調(diào)用CWnd::FromHandle,以創(chuàng)建一個(gè)臨時(shí)的CWnd對(duì)象。如果線程B需要更持久的連接,就可以使用Attach方法,在窗口及其CWnd對(duì)象之間建立持久的關(guān)聯(lián)。
另外的一個(gè)常見問題是MFC對(duì)象訪存的線程安全性問題。MFC對(duì)象不會(huì)自動(dòng)在不同的線程之間做出判斷。所以,如果兩個(gè)線程試圖同時(shí)訪問同一個(gè)CString類的對(duì)象,結(jié)果可能受到嚴(yán)重破壞。只有防止來(lái)自有沖突的MFC對(duì)象的線程。通常,這將需要使用前面提到的同步機(jī)制,以保證多線程數(shù)據(jù)交換的一致性。
?
參考:
《為什么要用TLS》
《WIN32下線程和窗口的數(shù)據(jù)綁定》
總結(jié)
以上是生活随笔為你收集整理的Win32多线程编程(5) — 线程局部存储的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Win32多线程编程(3) — 线程同步
- 下一篇: Win32多线程编程(6) — 多线程协