MFC不能多线程操作控件的原因
對于大多數(shù)mfc對象,請不要在線程間傳遞它們,不管是棧上的還是堆上的!原因如下:
??mfc的大多數(shù)類不是線程安全的,調(diào)用傳入對象的成員函數(shù)可能不會(huì)報(bào)錯(cuò),但是未必能達(dá)到程序預(yù)定的功能!
??mfc與界面有關(guān)的類,其大多數(shù)成員方法都是通過sendmessage實(shí)現(xiàn)的,如果消息處理函數(shù)本身不是線程安全的,你從工作線程中調(diào)用這些方法遲早會(huì)同你界面線程的用戶消息響應(yīng)發(fā)生沖突;
??對于CWnd相關(guān)的類,即使傳入窗口句柄,有時(shí)操作也會(huì)引起異常(ASSERT異常):通過句柄獲取窗口對象并且調(diào)用其成員函數(shù)或者成員變量!因?yàn)樵搶ο笫桥R時(shí)對象,訪問其成員變量沒有意義,訪問其成員函數(shù)可能會(huì)拋出異常!
??不能在線程間傳遞mfc對象的詳細(xì)分析很麻煩,涉及到MFC程序中的三種狀態(tài):模塊狀態(tài)、進(jìn)程狀態(tài)以及線程狀態(tài)!
??下面轉(zhuǎn)載了相關(guān)的文章,原文:《MFC多線程編程》
表現(xiàn)——錯(cuò)誤示例
關(guān)于啟動(dòng)線程時(shí)傳輸窗口對象(指針?句柄?)的問題:
在選擇菜單中的開始線程后:
void cmainframe::onmenu_start() {... afxbeginthread(mythread, this); ... }- 1
- 2
- 3
- 4
- 5
- 6
線程函數(shù)如下:
uint mythread(lpvoid pparam) {cmainframe* pmainfrm = (cmainframe *)pparam; ... }- 1
- 2
- 3
- 4
- 5
問題一:
??這樣的代碼是不是有問題? (文檔中說線程間不能直接傳輸mfc對象的指針,應(yīng)該通過傳輸句柄實(shí)現(xiàn))
問題二:
??這樣使用開始好像沒有問題,直接通過pmainfrm訪問窗口中的view都正常。 但發(fā)現(xiàn)訪問狀態(tài)條時(shí):
- 1
出現(xiàn)debug assertion failed!(在窗口線程中沒有問題) 位置是wincore.cpp中的
assert((p = pmap->lookuppermanent(m_hwnd)) != null || (p = pmap->lookuptemporary(m_hwnd)) != null);- 1
- 2
??為什么訪問view能正常,但訪問狀態(tài)條時(shí)不可以呢?
問題三:
??如果通過傳輸句柄實(shí)現(xiàn),怎樣做呢?我用下面的代碼執(zhí)行時(shí)有問題:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
??執(zhí)行時(shí)通過線程中得到pmainfrm,訪問其成員時(shí)不正常。
網(wǎng)友:hewwatt大致原因解釋如下:
??mfc的大多數(shù)類不是線程安全的,cwnd及其消息路由是其中之最
??mfc界面類的大多數(shù)方法,最后都是通過sendmessage實(shí)現(xiàn)的,而消息處理的過程中會(huì)引發(fā)其他消息的發(fā)送及處理。如果消息處理函數(shù)本身不是線程安全的你從工作線程中調(diào)用這些方法遲早會(huì)同你界面線程的用戶消息響應(yīng)發(fā)生沖突。
??cxxxx::fromhandle會(huì)根據(jù)調(diào)用者所在線程查表,如果查不到用戶創(chuàng)建的cxxxx對應(yīng)對象,它會(huì)創(chuàng)建一個(gè)臨時(shí)對象出來。由于你在工作線程中調(diào)用該方法,當(dāng)然不可能查到界面主線程中你所建立起來的那個(gè)對象了。這時(shí)mfc會(huì)你創(chuàng)建一個(gè)臨時(shí)對象并返回給你,你根本不可能期望它的成員變量會(huì)是有意義的。 所以要用 也只能用cwnd::fromhandle,因?yàn)樗话粋€(gè)m_hwnd成員。 不過,要記住跨線程直接或間接地調(diào)用::sendmessage,通常都是行為不可預(yù)測的。
原因分析:
??MFC界面包裝類(多線程時(shí)成員函數(shù)調(diào)用的斷言失敗)
經(jīng)常在論壇上看到如下的問題:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
??注意上面注釋中的兩處斷言失敗,本文從MFC底層的實(shí)現(xiàn)來解釋為什么會(huì)斷言失敗,并說明MFC為什么要這樣實(shí)現(xiàn)及相應(yīng)的處理辦法。
??在說明MFC界面包裝類的底層實(shí)現(xiàn)之前,由于其和窗口有關(guān),故先講解窗口類這個(gè)基礎(chǔ)知識以為后面做鋪墊。
窗口類
??窗口類是一個(gè)結(jié)構(gòu),其一個(gè)實(shí)例代表著一個(gè)窗口類型,與C++中的類的概念非常相近(雖然其表現(xiàn)形式完全不同,C++的類只不過是內(nèi)存布局和其上的操作這個(gè)概念的類型),故被稱作為窗口類。
??窗口是具有設(shè)備操作能力的邏輯概念,即一種能操作設(shè)備(通常是顯示器)的東西。由于窗口是窗口類的實(shí)例,就象C++中的一個(gè)類的實(shí)例,是可以具有成員函數(shù)的(雖然表現(xiàn)形式不同),但一定要明確窗口的目的——操作設(shè)備(這點(diǎn)也可以從Microsoft針對窗口所制訂的API的功能看出,主要出于對設(shè)備操作的方便)。因此不應(yīng)因?yàn)槠渚哂谐蓡T函數(shù)的功能而將窗口用于功能對象的創(chuàng)建,這雖然不錯(cuò),但是嚴(yán)重違反了語義的需要(關(guān)于語義,可參考我的另一篇文章——《語義的需要》),是不提倡的,但卻由于MFC界面包裝類的加入導(dǎo)致大多數(shù)程序員經(jīng)常將邏輯混入界面。
??窗口類是個(gè)結(jié)構(gòu),其中的大部分成員都沒什么重要意義,只是Microsoft一相情愿制訂的,如果不想使用界面API(Windows User Interface API),可以不管那些成員。其中只有一個(gè)成員是重要的——lpfnWndProc,消息處理函數(shù)。
??外界(使用窗口的代碼)只能通過消息操作窗口,這就如同C++中編寫的具有良好的面向?qū)ο箫L(fēng)格的類的實(shí)例只能通過其公共成員函數(shù)對其進(jìn)行操作。因此消息處理函數(shù)就代表了一個(gè)窗口的一切(忽略窗口類中其他成員的作用)。很容易發(fā)現(xiàn),窗口這個(gè)實(shí)例只具有成員函數(shù)(消息處理函數(shù)),不具有成員變量,即沒有一塊特定內(nèi)存和一特定的窗口相關(guān)聯(lián),則窗口將不能具有狀態(tài)(Windows還是提供了Window Properties API來緩和這種狀況)。這也正是上面問題發(fā)生的根源。
??為了處理窗口不能具有狀態(tài)的問題(這其實(shí)正是Windows靈活的表現(xiàn)),可以有很多種方法,而MFC出于能夠很容易的對已有窗口類進(jìn)行擴(kuò)展,選擇了使用一個(gè)映射將一個(gè)窗口句柄(窗口的唯一標(biāo)示符)和一個(gè)內(nèi)存塊進(jìn)行綁定,而這塊內(nèi)存塊就是我們熟知的MFC界面包裝類(從CWnd開始派生延續(xù))的實(shí)例。
MFC狀態(tài)
??狀態(tài)就是實(shí)例通過某種手段使得信息可以跨時(shí)間段重現(xiàn),C++的類的實(shí)例就是由外界通過公共成員函數(shù)改變實(shí)例的成員變量的值以實(shí)現(xiàn)具有狀態(tài)的效果。在MFC 中,具有三種狀態(tài):模塊狀態(tài)、進(jìn)程狀態(tài)、線程狀態(tài)。分別為模塊、進(jìn)程和線程這三種實(shí)例的狀態(tài)。由于代碼是由線程運(yùn)行,且和另外兩個(gè)的關(guān)系也很密切,因此也被稱作本地?cái)?shù)據(jù)。
模塊本地?cái)?shù)據(jù)
??具有模塊本地性的變量。模塊指一個(gè)加載到進(jìn)程虛擬內(nèi)存空間中的PE文件,即exe文件本身和其加載的dll文件。而模塊本地性即同樣的指針,根據(jù)代碼從不同的模塊執(zhí)行而訪問不同的內(nèi)存空間。這其實(shí)只用每個(gè)模塊都聲明一個(gè)全局變量,而前面的“代碼”就在MFC庫文件中,然后通過一個(gè)切換的過程(將欲使用的模塊的那個(gè)全局變量的地址賦給前述的指針)即可實(shí)現(xiàn)模塊本地性。MFC中,這個(gè)過程是通過調(diào)用AfxSetModuleState來切換的,而通常都使用 AFX_MANAGE_STATE這個(gè)宏來處理,因此下面常見的語句就是用于模塊狀態(tài)的切換的:
AFX_MANAGE_STATE( AfxGetStaticModuleState() );- 1
??MFC中定義了一個(gè)結(jié)構(gòu)(AFX_MODULE_STATE),其實(shí)例具有模塊本地性,記錄了此模塊的全局應(yīng)用程序?qū)ο笾羔槨①Y源句柄等模塊級的全局變量。其中有一個(gè)成員變量是線程本地?cái)?shù)據(jù),類型為AFX_MODULE_THREAD_STATE,其就是本文問題的關(guān)鍵。
進(jìn)程本地?cái)?shù)據(jù)
??具有進(jìn)程本地性的變量。與模塊本地性相同,即同一個(gè)指針,在不同進(jìn)程中指向不同的內(nèi)存空間。這一點(diǎn)Windows本身的虛擬內(nèi)存空間這個(gè)機(jī)制已經(jīng)實(shí)現(xiàn)了,不過在dll中定義的全局變量,如果dll支持Win32s,則其是共享其全局變量的,即不同的進(jìn)程加載了同一dll將訪問同一內(nèi)存。Win32s是為了那些基于Win32的應(yīng)用程序能在Windows 3.1上運(yùn)行,由于Windows 3.1是16位操作系統(tǒng),早已被淘汰,而現(xiàn)行的dll模型其本身就已經(jīng)實(shí)現(xiàn)了進(jìn)程本地性(不過還是可以通過共享節(jié)來實(shí)現(xiàn)Win32s中的dll的效果),因此進(jìn)程狀態(tài)其實(shí)就是一全局變量。
??MFC中作為本地?cái)?shù)據(jù)的結(jié)構(gòu)有很多,_AFX_WIN_STATE、_AFX_DEBUG_STATE、_AFX_DB_STATE等,都是MFC內(nèi)部自己使用的具有進(jìn)程本地性的全局變量。
線程本地?cái)?shù)據(jù)
??具有線程本地性的變量。如上,即同一個(gè)指針,不同的線程將會(huì)訪問不同的內(nèi)存空間。這點(diǎn)MFC是通過線程本地存儲(chǔ)(TLS——Thread Local Storage,其使用方法由于與本文無關(guān),在此不表)實(shí)現(xiàn)的。
??MFC中定義了一個(gè)結(jié)構(gòu)(_AFX_THREAD_STATE)以記錄某些線程級的全局變量,如最近一次的模塊狀態(tài)指針,最近一次的消息等。
模塊線程狀態(tài)
??MFC中定義的一個(gè)結(jié)構(gòu)(AFX_MODULE_THREAD_STATE),其實(shí)例即具有線程本地性又具有模塊本地性。也就是說不同的線程從同一模塊中和同一線程從不同模塊中訪問MFC庫函數(shù)都將導(dǎo)致操作不同的內(nèi)存空間。其應(yīng)用在AFX_MODULE_STATE中,記錄一些線程相關(guān)但又模塊級的數(shù)據(jù),如本文的重點(diǎn)——窗口句柄映射。
包裝類對象和句柄映射
??句柄映射——CHandleMap,MFC提供的一個(gè)底層輔助類,程序員是不應(yīng)該直接使用它的。其有兩個(gè)重要的成員變量:CMapPtrToPtr m_permanentMap, m_temporaryMap;。分別記錄永久句柄綁定和臨時(shí)句柄綁定。前面說過,MFC使用一個(gè)映射將窗口句柄和其包裝類的實(shí)例綁定在一起,m_permanentMap和m_temporaryMap就是這個(gè)映射,分別映射永久包裝類對象和臨時(shí)包裝類對象,而在前面提到過的 AFX_MODULE_THREAD_STATE中就有一個(gè)成員變量:CHandleMap* m_pmapHWND;(之所以是CHandleMap*是使用懶惰編程法,盡量節(jié)約資源)以專門完成HWND的綁定映射,除此以外還有如 m_pmapHDC、m_pmapHMENU等成員變量以分別實(shí)現(xiàn)HDC、HMENU的綁頂映射。而為什么這些映射要放在模塊線程狀態(tài)而不放在線程狀態(tài)或模塊狀態(tài)是很明顯的——這些包裝類包裝的句柄都是和線程相關(guān)的(如HWND只有創(chuàng)建它的線程才能接收其消息)且這個(gè)模塊中的包裝類對象可能不同于另一個(gè)模塊的(如包裝類是某個(gè)DLL中專門派生的一個(gè)類,如a.dll中定義的CAButton的實(shí)例和b.dll中定義的CBButton的實(shí)例如果同時(shí)在一個(gè)線程中。此時(shí)線程卸載了a.dll,然后CAButton的實(shí)例得到消息并進(jìn)行處理,將發(fā)生嚴(yán)重錯(cuò)誤——類代碼已經(jīng)被卸載掉了)。
??包裝類存在的意義有二:包裝對HWND的操作以加速代碼的編寫和提供窗口子類化(不是超類化)的效果以派生窗口類。包裝類對象針對線程分為兩種:永久包裝類對象(以后簡稱永久對象)和臨時(shí)包裝類對象(以后簡稱臨時(shí)對象)。臨時(shí)對象的意義僅僅只有包裝對HWND的操作以加速代碼編寫,不具有派生窗口類的功能。永久對象則具有前面說的包裝類的兩個(gè)意義。
??在創(chuàng)建窗口時(shí)(即CWnd::CreateEx中),MFC通過鉤子提前(WM_CREATE和WM_NCCREATE之前)處理了通知,用AfxWndProc子類化了創(chuàng)建的窗口并將對應(yīng)的CWnd*加入當(dāng)前線程的永久對象的映射中,而在AfxWndProc中,總是由CWnd::FromHandlePermanent(獲得對應(yīng)HWND的永久對象)得到當(dāng)前線程中當(dāng)前消息所屬窗口句柄對應(yīng)的永久對象,然后通過調(diào)用得到的CWnd*的WindowProc成員函數(shù)來處理消息以實(shí)現(xiàn)派生窗口類的效果。這也就是說永久對象具有窗口子類化的意義,而不僅僅是封裝HWND的操作。
??要將一個(gè)HWND和一個(gè)已有的包裝類對象相關(guān)聯(lián),調(diào)用CWnd::Attach將此包裝類對象和HWND映射成永久對象(但這種方法得到的永久對象不一定具有子類化功能,很可能仍和臨時(shí)對象一樣,僅僅起封裝的目的)。如果想得到臨時(shí)對象,則通過CWnd::FromHandle這個(gè)靜態(tài)成員函數(shù)以獲得。臨時(shí)對象之所以叫臨時(shí),就是其是由MFC內(nèi)部(CHandleMap::FromHandle)生成,其內(nèi)部(CHandleMap::DeleteTemp)銷毀(一般通過CWinThread::OnIdle中調(diào)用AfxUnlockTempMaps)。因此程序員是永遠(yuǎn)不應(yīng)該試圖銷毀臨時(shí)對象的(即使臨時(shí)對象所屬線程沒有消息循環(huán),不能調(diào)用CwinThread::OnIdle,在線程結(jié)束時(shí),CHandleMap的析構(gòu)仍然會(huì)銷毀臨時(shí)對象)。
原因
??為什么要分兩種包裝類對象?很好玩嗎?注意前面提過的窗口模型——只能通過消息機(jī)制和窗口交互。注意,也就是說窗口是線程安全的實(shí)例。窗口過程的編寫中不用考慮會(huì)有多個(gè)線程同時(shí)訪問窗口的狀態(tài)。如果不使用兩種包裝類對象,在窗口創(chuàng)建的鉤子中通過調(diào)用SetProp將創(chuàng)建的窗口句柄和對應(yīng)的CWnd*綁定,不一樣也可以實(shí)現(xiàn)前面說的窗口句柄和內(nèi)存塊的綁定?
??CWnd的派生類CA,具有一個(gè)成員變量m_BGColor以決定使用什么顏色填充底背景。線程1創(chuàng)建了CA的一個(gè)實(shí)例a,將其指針傳進(jìn)線程2,線程2設(shè)置a.m_BGColor為紅色。這已經(jīng)很明顯了,CA::m_BGColor不是線程安全的,如果不止一個(gè)線程2,那么a.m_BGColor將會(huì)出現(xiàn)線程訪問沖突。這嚴(yán)重違背窗口是線程安全的這個(gè)要求。因?yàn)槭褂昧朔窍C(jī)制與窗口進(jìn)行交互,所以失敗。
??繼續(xù),如果給CA一個(gè)公共成員函數(shù)SetBGColor,并在其中使用原子操作以保護(hù)m_BGColor,不就一切正常了?呵,在CA::OnPaint中,會(huì)兩次使用m_BGColor進(jìn)行繪圖,如果在兩次繪圖之間另一線程調(diào)用CA::SetBGColor改變了CA::m_BGColor,問題嚴(yán)重了。也就是說不光是CA::m_BGColor的寫操作需要保護(hù),讀操作亦需要保護(hù),而這僅僅是一個(gè)成員變量。
??那么再繼續(xù),完全按照窗口本身的定義,只使用消息與它交互,也就是說自定義一個(gè)消息,如AM_SETBGCOLOR,然后在CA::SetBGColor中SendMessage這個(gè)消息,并在其響應(yīng)函數(shù)中修改CA::m_BGColor。完美了,這是即符合窗口概念又很好的設(shè)計(jì),不過它要求每一個(gè)程序員編寫每一個(gè)包裝類時(shí)都必須注意到這點(diǎn),并且最重要的是,C++類的概念在這個(gè)設(shè)計(jì)中根本沒有發(fā)揮作用,嚴(yán)重地資源浪費(fèi)。
??因此,MFC決定要發(fā)揮C++類的概念的優(yōu)勢,讓包裝類對象看起來就等同于窗口本身,因此使用了上面的兩種包裝類對象。讓包裝類對象隨線程的不同而不同可以對包裝類對象進(jìn)行線程保護(hù),也就是說一個(gè)線程不可以也不應(yīng)該訪問另一個(gè)線程中的包裝類對象(因?yàn)榘b類對象就相當(dāng)于窗口,這是MFC的目標(biāo),并不是包裝類本身不能被跨線程訪問),“不可以”就是通過在包裝類成員函數(shù)中的斷言宏實(shí)現(xiàn)的(在CWnd::AssertValid中),而“不應(yīng)該”前面已經(jīng)解釋地很清楚了。因此本文開頭的斷言失敗的根本原因就是因?yàn)檫`反了“不可以”和“不應(yīng)該”。
??雖然包裝類對象不能跨線程訪問,但是窗口句柄卻可以跨線程訪問。因?yàn)榘b類對象不僅等同于窗口,還改變了窗口的交互方式(這也正是C++類的概念的應(yīng)用),使得不用非得使用消息機(jī)制才能和窗口交互。注意前面提到的,如果跨線程訪問包裝類對象,而又使用C++類的概念操作它,則其必須進(jìn)行線程保護(hù),而“不能跨線程訪問”就消除了這個(gè)問題。因此臨時(shí)對象的產(chǎn)生就只是如前面所說,方便代碼的編寫而已,不提供子類化的效果,因?yàn)榇翱诰浔梢钥缇€程訪問。
解決辦法
??已經(jīng)了解失敗的原因,因此做如下修改:
DWORD WINAPI ThreadProc( void *pData ) // 線程函數(shù)(比如用于從COM口獲取數(shù)據(jù)) {// 數(shù)據(jù)獲取循環(huán)// 數(shù)據(jù)獲得后放在變量i中CAbcDialog *pDialog = static_cast< CAbcDialog* >(CWnd::FromHandle( reinterpret_cast< HWND >( pData ) ) );ASSERT_VALID( pDialog ); // 此處可能斷言失敗pDialog->m_Data = i; // 這是不好的設(shè)計(jì),詳情可參看我的另一篇文章:《語義的需要》pDialog->UpdateData( FALSE ); // UpdateData內(nèi)部ASSERT_VALID( this )可能斷言失敗… } BOOL CAbcDialog::OnInitDialog() {CDialog::OnInitDialog();// 其他初始化代碼CreateThread( NULL, 0, ThreadProc, m_hWnd, 0, NULL ); // 創(chuàng)建線程return TRUE; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
??之所以是“可能”,因?yàn)檫@里有個(gè)重點(diǎn)就是臨時(shí)對象是HWND操作的封裝,不是窗口類的封裝。因此所有的HWND臨時(shí)對象都是CWnd的實(shí)例,即使上面強(qiáng)行轉(zhuǎn)換為CAbcDialog*也依舊是CWnd*,所以在ASSERT_VALID里調(diào)用CAbcDialog::AssertValid時(shí),其定義了一些附加檢查,則可能發(fā)現(xiàn)這是一個(gè)CWnd的實(shí)例而非一個(gè)CAbcDialog實(shí)例,導(dǎo)致斷言失敗。因此應(yīng)將CAbcDialog全部換成CWnd,這下雖然不斷言失敗了,但依舊錯(cuò)誤(先不提pDialog->m_Data怎么辦),因?yàn)榕R時(shí)對象是HWND操作的封裝,而不幸的是UpdateData只是MFC自己提供的一個(gè)對話框數(shù)據(jù)交換的機(jī)制(DDX)的操作,其不是通過向HWND發(fā)送消息來實(shí)現(xiàn)的,而是通過虛函數(shù)機(jī)制。因此在UpdateData中調(diào)用實(shí)例的DoDataExchange將不能調(diào)用CAbcDialog::DoDataExchange,而是調(diào)用CWnd::DoDataExchange,因此將不發(fā)生任何事。
??因此合理(并不一定最好)的解決方法是向CAbcDialog的實(shí)例發(fā)送一個(gè)消息,而通過一個(gè)中間變量(如一全局變量)來傳遞數(shù)據(jù),而不是使用CAbcDialog::m_Data。當(dāng)然,如果數(shù)據(jù)少,比如本例,就應(yīng)該將數(shù)據(jù)作為消息參數(shù)進(jìn)行傳遞,減少代碼的復(fù)雜性;數(shù)據(jù)多則應(yīng)該通過全局變量傳遞,減少了緩沖的管理費(fèi)用。修改后如下:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
注意事項(xiàng)
??“線程安全”是一個(gè)什么概念?
??以前常聽高手告誡MFC對象不要跨線程使用,因?yàn)镸FC不是線程安全的。比如CWnd對象不要跨線程使用,可以用窗口句柄(HWND)代替。CSocket/CAsyncSocket對象不要跨線程使用,用SOCKET句柄代替.那么到底什么是線程安全呢?什么時(shí)候需要考慮?如果程序涉及到多線程的話,就應(yīng)該考慮線程安全問題。比如說設(shè)計(jì)的接口,將來需要在多線程環(huán)境中使用,或者需要跨線程使用某個(gè)對象時(shí),這個(gè)就必須考慮了。關(guān)于線程安全也沒什么權(quán)威定義。在這里我只說說我的理解:所提供的接口對于線程來說是原子操作或者多個(gè)線程之間的切換不會(huì)導(dǎo)致該接口的執(zhí)行結(jié)果存在二義性,也就是說我們不用考慮同步的問題。
??一般而言“線程安全”由多線程對共享資源的訪問引起。如果調(diào)用某個(gè)接口時(shí)需要我們自己采取同步措施來保護(hù)該接口訪問的共享資源,則這樣的接口不是線程安全的.MFC和STL都不是線程安全的. 怎樣才能設(shè)計(jì)出線程安全的類或者接口呢?如果接口中訪問的數(shù)據(jù)都屬于私有數(shù)據(jù),那么這樣的接口是線程安全的.或者幾個(gè)接口對共享數(shù)據(jù)都是只讀操作,那么這樣的接口也是線程安全的.如果多個(gè)接口之間有共享數(shù)據(jù),而且有讀有寫的話,如果設(shè)計(jì)者自己采取了同步措施,調(diào)用者不需要考慮數(shù)據(jù)同步問題,則這樣的接口是線程安全的,否則不是線程安全的。
多線程的程序設(shè)計(jì)應(yīng)該注意些什么呢
??盡量少的使用全局變量、static變量做共享數(shù)據(jù),盡量使用參數(shù)傳遞對象。被參數(shù)傳遞的對象,應(yīng)該只包括必需的成員變量。所謂必需的成員變量,就是必定會(huì)被多線程操作的。很多人圖省事,會(huì)把this指針(可能是任意一個(gè)對象指針)當(dāng)作線程參數(shù)傳遞,致使線程內(nèi)部有過多的操作權(quán)限,對this中的參數(shù)任意妄為。整個(gè)程序由一個(gè)人完成,可能會(huì)非常注意,不會(huì)出錯(cuò),但只要一轉(zhuǎn)手,程序就會(huì)面目全非。當(dāng)兩個(gè)線程同時(shí)操作一個(gè)成員變量的時(shí)候,程序就開始崩潰了,更糟的是,這種錯(cuò)誤很難被重現(xiàn)。(我就在郁悶這個(gè)問題,我們是幾個(gè)人,把程序編成debug版,經(jīng)過數(shù)天使用,才找到錯(cuò)誤。而找到錯(cuò)誤只是開始,因?yàn)槟阋C明這個(gè)bug被修改成功了,也非常困難。)其實(shí),線程間數(shù)據(jù)交互大多是單向的,在線程回調(diào)函數(shù)入口處,盡可能的將傳入的數(shù)據(jù)備份到局部變量中(當(dāng)然,用于線程間通訊的變量不能這么處理),以后只對局部變量做處理,可以很好的解決這種問題。
??在MFC中請慎用線程。因?yàn)镸FC的框架假定你的消息處理都是在主線程中完成的。首先窗口句柄是屬于線程的,如果擁有窗口句柄的線程退出了,如果另一個(gè)線程處理這個(gè)窗口句柄,系統(tǒng)就會(huì)出現(xiàn)問題。而MFC為了避免這種情況的發(fā)生,使你在子線程中調(diào)用消息(窗口)處理函數(shù)時(shí),就會(huì)不停的出Assert錯(cuò)誤,煩都煩死你。典型的例子就時(shí)CSocket,因?yàn)镃Socket是使用了一個(gè)隱藏窗口實(shí)現(xiàn)了假阻塞,所以不可避免的使用了消息處理函數(shù),如果你在子線程中使用CSocket,你就可能看到assert的彈出了。
??不要在不同的線程中同時(shí)注冊COM組件。兩個(gè)線程,一個(gè)注冊1.ocx, 2.ocx, 3.ocx, 4.ocx; 而另一個(gè)則注冊5.ocx, 6.ocx, 7.ocx, 8.ocx,結(jié)果死鎖發(fā)生了,分別死在FreeLibrary和DllRegisterServer,因?yàn)檫@8個(gè)ocx是用MFC中做的,也可能是MFC的Bug,但DllRegisterServer卻死在GetModuleFileName里,而GetModuleFileName則是個(gè)API唉!如果有過客看到,恰巧又知道其原因,請不吝賜教。
??不要把線程搞的那么復(fù)雜。很多初學(xué)者,恨不能用上線程相關(guān)的所有的函數(shù),這里互斥,那里等待,一會(huì)兒起線程,一會(huì)兒關(guān)線程的,比起goto語句有過之而無不及。好的多線程程序,應(yīng)該是盡量少的使用線程。這句話怎么理解吶,就是說盡量統(tǒng)一一塊數(shù)據(jù)共享區(qū)存放數(shù)據(jù)隊(duì)列,工作子線程從隊(duì)列中取數(shù)據(jù),處理,再放回?cái)?shù)據(jù),這樣才會(huì)模塊化,對象化;而不是每個(gè)數(shù)據(jù)都起一個(gè)工作子線程處理,處理完了就關(guān)閉,寫的時(shí)候雖然直接,等維護(hù)起來就累了。
后記:
在弄 gh0st 的status bar 的時(shí)候遇到了一個(gè)錯(cuò)誤,正好看到了這篇文章。感覺分析很好 ,就轉(zhuǎn)載了。
總結(jié)
以上是生活随笔為你收集整理的MFC不能多线程操作控件的原因的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: FFMPEG 源码分析
- 下一篇: 项目团队要以十当一