1,表現——錯誤示例
關於啟動執行緒時傳輸視窗物件(指標?控制代碼?)的問題:
在選擇選單中的開始執行緒後:
void cmainframe::onmenu_start()
執行緒函式如下:
uint mythread(lpvoid pparam)
問題一:
這樣的**是不是有問題?
(文件中說執行緒間不能直接傳輸mfc物件的指標,應該通過傳輸控制代碼實現)
問題二:
這樣使用開始好像沒有問題,直接通過pmainfrm訪問視窗中的view都正常。
但發現訪問狀態條時:
pmainfrm->m_wndstatusbar.setpanetext(2, "test);
出現debug assertion failed!(在視窗執行緒中沒有問題)
位置是wincore.cpp中的
assert((p = pmap->lookuppermanent(m_hwnd)) != null ||
(p = pmap->lookuptemporary(m_hwnd)) != null);
為什麼訪問view能正常,但訪問狀態條時不可以呢?
問題三:
如果通過傳輸控制代碼實現,怎樣做呢?
我用下面的**執行時有問題:
void cmainframe::onmenu_start()
uint mythread(lpvoid pparam)
執行時通過執行緒中得到pmainfrm,訪問其成員時不正常。
2. 原因分析
mfc介面包裝類(多執行緒時成員函式呼叫的斷言失敗)
日期:2006-9-17 18:06:00 [host01.com]
mfc介面包裝類
——多執行緒時成員函式呼叫的斷言失敗
經常在論壇上看到如下的問題:
dword winapi threadproc( void *pdata ) // 執行緒函式(比如用於從com口獲取資料)
bool cabcdialog::oninitdialog()
注意上面注釋中的兩處斷言失敗,本文從mfc底層的實現來解釋為什麼會斷言失敗,並說明mfc為什麼要這樣實現及相應的處理辦法。
在說明mfc介面包裝類的底層實現之前,由於其和視窗有關,故先講解視窗類這個基礎知識以為後面做鋪墊。
視窗類視窗類是乙個結構,其乙個例項代表著乙個視窗型別,與c++中的類的概念非常相近(雖然其表現形式完全不同,c++的類只不過是記憶體布局和其上的操作這個概念的型別),故被稱作為視窗類。
視窗是具有裝置操作能力的邏輯概念,即一種能操作裝置(通常是顯示器)的東西。由於視窗是視窗類的例項,就象c++中的乙個類的例項,是可以具有成員函式的(雖然表現形式不同),但一定要明確視窗的目的——操作裝置(這點也可以從microsoft針對視窗所制訂的api的功能看出,主要出於對裝置操作的方便)。因此不應因為其具有成員函式的功能而將視窗用於功能物件的建立,這雖然不錯,但是嚴重違反了語義的需要(關於語義,可參考我的另一篇文章——《語義的需要》),是不提倡的,但卻由於mfc介面包裝類的加入導致大多數程式設計師經常將邏輯混入介面。
視窗類是個結構,其中的大部分成員都沒什麼重要意義,只是microsoft一相情願制訂的,如果不想使用介面api(windows user inte***ce 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() );
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中,記錄一些執行緒相關但又模組級的資料,如本文的重點——視窗控制代碼對映。
原因為什麼要分兩種包裝類物件?很好玩嗎?注意前面提過的視窗模型——只能通過訊息機制和窗**互。注意,也就是說視窗是執行緒安全的例項。視窗過程的編寫中不用考慮會有多個執行緒同時訪問視窗的狀態。如果不使用兩種包裝類物件,在視窗建立的鉤子中通過呼叫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++類的概念操作它,則其必須進行執行緒保護,而「不能跨執行緒訪問」就消除了這個問題。因此臨時物件的產生就只是如前面所說,方便**的編寫而已,不提供子類化的效果,因為視窗控制代碼可以跨執行緒訪問。
MFC多執行緒程式設計注意事項
視窗類視窗類是乙個結構,其乙個例項代表著乙個視窗型別,與c 中的類的概念非常相近 雖然其表現形式完全不同,c 的類只不過是記憶體布局和其上的操作這個概念的型別 故被稱作為視窗類。視窗是具有裝置操作能力的邏輯概念,即一種能操作裝置 通常是顯示器 的東西。由於視窗是視窗類的例項,就象c 中的乙個類的例項...
多執行緒程式設計注意事項
1 明確目的,為什麼要使用多執行緒?如果是由於單執行緒讀寫或者網路訪問 例如http訪問網際網路 的瓶頸,可以考慮使用執行緒池。如果是對不同的資源 例如socket連線 進行管理,可以考慮多個執行緒。2 執行緒使用中要注意,如何控制線程的排程和阻塞,例如利用事件的觸發來控制線程的排程和阻塞,也有用訊...
多執行緒程式設計的注意事項
多執行緒程式設計的注意事項 1 明確目的,為什麼要使用多執行緒?如果是由於單執行緒讀寫或者網路訪問 例如http訪問網際網路 的瓶頸,可以考慮使用執行緒池。如果是對不同的資源 例如socket連線 進行管理,可以考慮多個執行緒。2 執行緒使用中要注意,如何控制線程的排程和阻塞,例如利用事件的觸發來控...