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