多线程和MsgWaitForMultipleObjects
在主線程中慎用WaitForSingleObject?(WaitForMultipleObjects)
下面的代碼我調試了將近一個星期,你能夠看出什么地方出了問題嗎?
線程函數:
DWORD?WINAPI?ThreadProc(
????while(!bTerminate)
????{
????????//?從一個鏈表中讀取信息并且插入到CListCtrl中
????????//?CListCtrl的句柄是通過線程參數傳遞進來的
????????for(;;)
???????{
???????????ReadInfoFromList();
???????????InsertToCListCtrl();
????????}
????}
}
主線程中使用CreateThread啟動線程。
?
當想終止子線程時,在主線程中:
bTerminate?=?TRUE;
WaitForSingleObject(threadHandle,?INFINITE);
可是,以運行到WaitForSingleObject,子線程就Crash了。
?
為什么呢?
?
問題原因:
后來我終于在InsertItem的反匯編中發現了如下的代碼
call?dword?ptr?[__imp__SendMessageA@16?(7C141B54h)]
可見,InsertItem是必須借助消息循環來完成任務的。如果我們在主線程中WaitForSingleObject了,必然導致主線程阻塞,也就導致了消息循環的阻塞,最終導致工作線程Crash掉了*_*
?
解決方案:
為了解決在主線程中Wait的問題,微軟專門設計了一個函數MsgWaitForMultipleObjects,這個函數即可以等待信號(thread,event,mutex等等),也可以等待消息(MSG)。即不論有信號被激發或者有消息到來,此函數都可以返回。呵呵,那么我的解決辦法也就出來了。
將上面的WaitForSingleObject用下面的代碼替換:
?
while(TRUE)
{
?
????DWORD?result?;?
????MSG?msg?;?
?
????result?=?MsgWaitForMultipleObjects(1,?&readThreadHandle,?
????????FALSE,?INFINITE,?QS_ALLINPUT);?
?
????if?(result?==?(WAIT_OBJECT_0))
????{
????????break;
????}?
????else?
????{?
????????PeekMessage(&msg,?NULL,?0,?0,?PM_REMOVE);
????????DispatchMessage(&msg);?
????}?
}
?
總結:
如果在工作線程中有可能涉及到了消息驅動的API,那么不能在主線程中使用WaitForSingleObject一類函數,而必須使用上述的方案。
?
線程函數的設計以及MsgWaitForMultipleObjects函數的使用要點
使用多線程技術可以顯著地提高程序性能,本文就講講在程序中如何使用工作線程,以及工作線程與主線程通訊的問題。
一?創建線程
???????使用MFC提供的全局函數AfxBeginThread()即可創建一個工作線程。線程函數的標準形式為?UINT?MyFunProc(LPVOID?);此函數既可以是全局函數,也可以是類的靜態成員函數。之所以必須是靜態成員函數,是由于類的非靜態成員函數,編譯器在編譯時會自動加上一個this指針參數,如果將函數設置為靜態的成員函數,則可以消除this指針參數。如果想在線程函數中任意調用類的成員變量(此處指的是數據成員,而不是控件關聯的成員變量),則可以將類的指針作為參數傳遞給線程函數,然后經由該指針,就可以調用類的成員變量了。
?
//線程函數,類的靜態成員函數
UINT?CThreadTest::TH_SetProgress(LPVOID?lpVoid)
{
???????CThreadTest?*pTest=(CThreadTest?*)lpVoid;
???????pTest->SetProgress();
???????return?0;
}
?
//類的成員函數,此函數執行實際的線程函數操作,卻可以自如的調用成員數據
void?CThreadTest::SetProgress()
{
int?nCount=0;
???????while?(1)
???????{
??????????????m_progress.SetPos(nCount);?//設置進度條進度
//????????????this->SendMessage(WM_SETPROGRESSPOS,nCount,0);//也可以采用這種方式設置
??????????????nCount++;
??????????????if?(g_exitThread)
??????????????{
?????????????????????return;
??????????????}
??????????????Sleep(200);
???????}
}
?
二?線程函數體的設計
有過多線程設計經驗的人都有體會,多線程設計最重要的就是要處理好線程間的同步和通訊問題。如解決不好這個問題,會給程序帶來潛藏的隱患。線程的同步可以利用臨界區、事件、互斥體和信號量來實現,線程間的通訊可利用全局變量和發消息的形式實現。其中事件和臨界區是使用得比較多的工具。請看下面的線程函數體:
UINT?AnalyseProc(LPVOID???lVOID)
{
???????if(WAIT_OBJECT_0==?WaitForSingleObject(m_eventStartAnalyse.m_hThread,INFINITE))
???????{
??????????????while?(WAIT_OBJECT_0?==?WaitForSingleObject(m_eventExitAnalyse.m_hThread,0))
??????????????{
?????????????????????DWORD?dRet=WaitForSingleObject(m_eventPause.m_hThread,0);
?????????????????????if?(dRet?==?WAIT_OBJECT_0)
?????????????????????{
????????????????????????????//暫停分析
????????????????????????????Sleep(10);
?????????????????????}
?????????????????????else?if?(dRet?==?WAIT_TIMEOUT)
?????????????????????{
????????????????????????????//繼續分析
????????????????????????????//
?????????????????????}
??????????????}
???????}
???????return?0;
}
上面的線程函數用到了三個事件變量eventStartAnalyse、eventExitAnalyse和eventPause,分別用來控制線程函數的啟動、退出以及暫停。再配以WaitForSingleObject函數,就可以自如的控制線程函數的執行,這是在線程函數體內應用事件變量的典型方式,也是推薦的方式。
?
無論是工作線程還是用戶界面線程,都有消息隊列,都可以接收別的線程發過來的消息也可以給別的線程發送消息。給工作線程發消息使用的函數是PostThreadMessage()。此函數的第一個參數是接收消息的線程的ID。此函數是異步執行的,機制和PostMessage一樣,就是把消息拋出后就立即返回,不理會消息是否被處理完了。
?
這里還有著重強調一點,線程消息隊列是操作系統幫我們維護的一種資源,所以它的容量也是有限制的。筆者曾經做過實驗,在5~6秒事件內調用PostThreadMessage往線程消息隊列里發送5萬多條消息,可是由于線程函數處理消息的速度遠慢于發送速度,結果導致線程消息隊列里已經堆滿了消息,而發送端還在發消息,最終導致消息隊列溢出,很多消息都丟失了。所以,如果你要在短時間內往線程消息隊列里發送很多條消息,那就要判斷一下PostThreadMessage函數的返回值。當消息隊列已經溢出時,此函數返回一個錯誤值。根據返回值,你就可以控制是否繼續發送。
?
工作線程給主線程發消息使用的是SendMessage和PoseMessage函數。這兩個函數的區別在于SendMessage函數是阻塞方式,而PoseMessage函數是非阻塞方式。如果不是嚴格要求工作線程與主線程必須同步執行,則推薦使用PoseMessage。不要在線程函數體內操作MFC控件,因為每個線程都有自己的線程模塊狀態映射表,在一個線程中操作另一個線程中創建的MFC對象,會帶來意想不到的問題。更不要在線程函數里,直接調用UpdataData()函數更新用戶界面,這會導致程序直接crash。而應該通過發送消息給主線程的方式,在主線程的消息響應函數里操作控件。上面提到的SetProgress函數和AnalyseProc函數均為線程函數,但它們都不能接收別的線程發過來的消息,雖然它們都可以給主線程發消息。它們要想能夠接收別的線程發過來的消息,則必須調用GetMessage或PeekMessage函數。這兩個函數的主要區別在于:
?
GetMessage函數可以從消息隊列中抓取消息,當抓取到消息后,GetMessage函數會將此條消息從消息隊列中刪除。而且,如果消息隊列中沒有消息,則GetMessage函數不會返回,CPU轉而回去執行別的線程,釋放控制權。GetMessage返回的條件是抓取的消息是WM_QUIT。
?
PeekMessage函數也可以從消息隊列中抓取消息,如果它的最后一個參數設置為PM_NOREMOVE,則不從消息隊列中刪除此條消息,此條消息會一直保留在消息隊列中。如果它的最后一個參數是PM_REMOVE,則會刪除此條消息。如果消息隊列中沒有消息,則PeekMessage函數會立刻返回,而不是像GetMessage一樣就那樣等在那兒。PeekMessage函數就像是窺探一下消息隊列,看看有沒有消息,有的話就處理,沒有就離開了。這一點也是兩個函數的最大不同。下面的代碼演示了在線程函數中使用這兩個函數的三種方式,這三種方法可以達到同樣的效果:
?
void?CThreadTest::SetSlider()
{
//?在線程函數里啟動一個時鐘,每50毫秒發送一個WM_TIMER消息
???????int?nTimerID=::SetTimer(NULL,1,50,NULL);
???????int?nSliderPos=0;
???????MSG?msg;
???????while?(1)
???????{
//方式一????使用GetMessage函數??
/*???????????if?(::GetMessage(&msg,NULL,0,0))
??????????????{
?????????????????????switch(msg.message)
?????????????????????{
?????????????????????case?WM_TIMER:
????????????????????????????{
???????????????????????????????????nSliderPos++;
?????????????????????????::SendMessage(this->m_hWnd,WM_SETSLIDERPOS,nSliderPos,0);
????????????????????????????}?????????????????????????
????????????????????????????break;
?
?????????????????????case?WM_QUIT_THREAD:?//自定義消息
????????????????????????????{
???????????????????????????????????::KillTimer(NULL,1);
???????????????????????????????????return;
????????????????????????????}??????????????????
?????????????????????????break;
?????????????????????default:
?????????????????????????break;
?????????????????????}
??????????????}????
?*/
?
//方式二???使用PeekMessage函數??
/*???????????if?(::PeekMessage(&msg,NULL,0,0,PM_REMOVE))
??????????????{
?????????????????????switch(msg.message)
?????????????????????{
?????????????????????case?WM_TIMER:
????????????????????????????{
???????????????????????????????????nSliderPos++;
???????????????????????????????????::SendMessage(this->m_hWnd,WM_SETSLIDERPOS,nSliderPos,0);
????????????????????????????}?????????????????????????
????????????????????????????break;
?????????????????????case?WM_QUIT_THREAD:?//自定義消息
????????????????????????????{
???????????????????????????????????::KillTimer(NULL,1);
???????????????????????????????????return;
????????????????????????????}??????????????????
?????????????????????????break;
?????????????????????default:
?????????????????????????break;
?????????????????????}
??????????????}
??????????????else
??????????????{
???????????????????????//必須有此操作,要不然當沒有消息到來時,線程函數相當于陷入空循環,cpu的占有率會飆升
?????????????????????Sleep(20);
??????????????}
?
*/
?
//方式三???同時使用PeekMessage和GetMessage函數??
??????????????if?(::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE))
??????????????{
?????????????????????if(::GetMessage(&msg,NULL,0,0))
?????????????????????{
????????????????????????????switch(msg.message)
????????????????????????????{
??????????????????????????case?WM_TIMER:
???????????????????????????????????{
??????????????????????????????????????????nSliderPos++;?
::SendMessage(this->m_hWnd,WM_SETSLIDERPOS,nSliderPos,0);
???????????????????????????????????}?????????????????????????
??????????????????????????????????break;
????????????????????????????case?WM_QUIT_THREAD:?//自定義消息
???????????????????????????????????{
??????????????????????????????????????????::KillTimer(NULL,1);
??????????????????????????????????????????return;
???????????????????????????????????}??????????????????
???????????????????????????????????break;
????????????????????????????default:
???????????????????????????????????break;
????????????????????????????}
?????????????????????}
??????????????}
??????????????else
??????????????{
?????????????????????Sleep(20);
??????????????}
???????}
?
}
?
前面已經介紹過了,不建議線程函數里用SendMessage給主線程發消息,因為這個函數是同步操作,就是如果SendMessage函數不執行完,是不會返回的,這樣線程函數就無法繼續執行。有時這種操作容易導致工作線程和主線程死鎖,這個我們后面會談到,會介紹一種解決方法。
?
三?線程的退出
線程的退出有多種方式,比如可以調用TerminateThread()函數強制線程退出,但不推薦這種方式,因為這樣做會導致線程中的資源來不及釋放。最好的也是推薦的方式,是讓線程函數自己退出。就像上面介紹的SetProgress()函數中,用全局變量g_exitThread使線程退出。
?
而AnalyseProc用WAIT_OBJECT_0?==WaitForSingleObject(m_eventExitAnalyse.m_hThread,0)這種方式來退出線程,還有在SetSlider函數中利用發送自定義消息WM_QUIT_THREAD的方式令線程退出。這些都是可以使用的方法。
?
???????當主線程要退出時,為了能保證線程的資源能全部地釋放,主線程必須等待工作線程退出。線程對象和進程對象一樣,也是內核對象,而且線程對象的特點是當線程退出時,線程內核對象會自動變為有信號狀態,能夠喚醒所有正在等待它的線程。我們通常都習慣于使用WaitForSingleObject等函數來等待某個內核對象變為有信號狀態,但是我想說的是,在主線程中不要使用WaitForSingleObject和WaitForMultipleObjects兩個函數等待線程退出,其原因就是有導致程序死鎖的隱患,特別是線程函數里調用了SendMessage或是直接操作了MFC對象,更易出現此種現象。下面的函數是一個在主線程中用來等待SetProgress()線程函數退出的函數:
?
//退出線程
void?CThreadTest::OnButton2()
{
???????g_exitThread=TRUE;?//設置全局變量為真,令線程退出
#if?1
???????WaitForSingleObject(m_pThread1->m_hThread,INFINITE);?//無限等待
#else
???????DWORD?dRet;
???????MSG?msg;
???????while?(1)
???????{
??????????????dRet=::MsgWaitForMultipleObjects(1,&m_pThread1->m_hThread,FALSE,INFINITE,QS_ALLINPUT);
??????????????if?(dRet?==?WAIT_OBJECT_0+1)
??????????????{
?????????????????????while?(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
?????????????????????{
????????????????????????????TranslateMessage(&msg);
????????????????????????????DispatchMessage(&msg);
?????????????????????}
??????????????}
??????????????else
??????????????{
?????????????????????break;
??????????????}
???????}?????
#endif????
}
?
在上面的函數中我用#if?#else?#endif這組預編譯指令控制函數的執行代碼,如果我令#if?1,則執行WaitForSingleObject函數,如果我令#if?0,則執行DWORD?dRet路徑。首先令#if??1,測試會發現,程序死鎖了。原因是當程序執行到WaitForSingleObject函數時,主線程掛起,等待線程函數退出,此時CPU切換到線程函數體內執行,如果執行到if?(g_exitThread)處,則線程函數順利退出,可如果執行到m_progress.SetPos(nCount)處,由于SetPos函數是在主線程中完成的操作,Windows是基于消息的操作系統,很多操作都是靠發消息完成的,由于主線程已經掛起,所以沒有機會去消息隊列中抓取消息并處理它,結果導致SetPos函數不會返回,工作線程也被掛起,典型的死鎖。如果不用m_progress.SetPos,而改用this->SendMessage(…),其結果是一樣的。此時如果用了PostMessage,則工作線程會順利退出,因為PostMessage是異步執行的。由此可見,在主線程中用WaitForSingleObject等待工作線程退出是有很大隱患的。
?
???????為解決這一問題,微軟特提供了一個MsgWaitForMultipleObjects函數,該函數的特點是它不但可以等待內核對象,還可以等消息。也就是當有消息到來時,該函數也一樣可以返回,并處理消息,這樣就給了工作線程退出的機會。
?
DWORD?MsgWaitForMultipleObjects(
DWORD?nCount,?//要等待的內核對象數目
LPHANDLE?pHandles,?//要等待的內核對象句柄數組指針
BOOL?fWaitAll,?//是等待全部對象還是單個對象
DWORD?dwMilliseconds,//等待時間?
DWORD?dwWakeMask?);//等待的消息類型
?
下面就詳解一下該函數的參數使用方法:
DWORD?nCount:要等待的內核對象的數目。如果等待兩個線程退出,則nCount=2;
LPHANDLE?pHandles:要等待的內核對象句柄數組指針。
?
如果只要等待一個線程退出,則直接設置該線程句柄的指針即可:
MsgWaitForMultipleObjects(1,&m_pThread->m_hThread,…)
?
如果要等待兩個線程退出,則使用方法為:
HANDLE?hArray[2]={?m_pThread1->m_hThread?,?m_pThread2->m_hThread?};
MsgWaitForMultipleObjects(2,hArray,…)
?
BOOL?fWaitAll:?TRUE-表示只有要等待的線程全部退出后,此函數才返回,
???????????????FALSE-表示要等待的線程中任意一個退出了,或是有消息到達了,此函數均會返回。
在上面的OnButton2()函數中,我要等待一個線程退出,將fWaitAll設置為
FALSE,目的是無論是線程真的退出了,還是有消息到達了,該函數都能返回。
如果將該fWaitAll設置為TRUE,那么函數返回的唯一條件是線程退出了,即便
是有消息到來了,該函數也一樣不會返回。
?
DWORD?dwMilliseconds:等待的事件,單位是毫秒??梢栽O置為INFINITE,無
窮等待
?
DWORD?dwWakeMask:等待的消息類型,通常可以設置為QS_ALLINPUT。此宏表示的是可以等待任意類型的消息。當然,也可以指定等待的消息類型。
?
#define?QS_ALLINPUT????????(QS_INPUT?????????|?\
????????????????????????????QS_POSTMESSAGE???|?\
????????????????????????????QS_TIMER?????????|?\
????????????????????????????QS_PAINT?????????|?\
????????????????????????????QS_HOTKEY????????|?\
????????????????????????????QS_SENDMESSAGE)
?
返回值:DWORD?dRet?通過函數返回值,可以得到一些有效信息。函數返回值依fWaitAll設置的不同而有所不同。下面是函數返回值的幾種常見類型:
?
dRet?=?0xFFFFFFFF?:???表示函數調用失敗,可用GetLastError()得到具體的出錯信息;
?
dRet?=WAIT_OBJECT_0+nCount:表示有消息到達了;
?
?
?
如果fWaitAll設置為TRUE
?
dRet?=?WAIT_OBJECT_0,表示所有等待的核心對象都激發了,或是線程都退出了;
?
如果fWaitAll設置為FALSE
?
dRet?=?WAIT_OBJECT_0?~?WAIT_OBJECT_0+nCount-1:表示等待的內核對象被激發了,index=dRet?-?WAIT_OBJECT_0,表示hArray[]數組中索引為index的那個對象被激發了。
?
?
?
當函數由于消息到來而返回,則需要用戶主動去消息隊列中將消息抓取出來,然后派發出去,這樣該消息就會被處理了。其具體的操作就是:
?
while?(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
?
{
?
???????TranslateMessage(&msg);
?
???????DispatchMessage(&msg);
?
}
?
?
?
下面再看一個用這個函數等待兩個線程退出的例子:
?
//關閉線程1和2
?
void?CThreadTest::OnButton6()
?
{
?
???????…
?
???????…
?
???????DWORD?dRet=-2;
?
???????HANDLE?hArray[2];?
?
??????
?
???????hArray[0]=m_pThread1->m_hThread;
?
???????hArray[1]=m_pThread2->m_hThread;
?
?
?
???????MSG?msg;
?
?
?
???????int?nExitThreadCount=0;???????//標記已經有幾個線程退出了
?
???????BOOL?bWaitAll=FALSE;
?
???????int?nWaitCount=2;????//初始等待的線程數目
?
?
?
???????while?(1)
?
???????{
?
??????????????dRet=MsgWaitForMultipleObjects(nWaitCount,hArray,bWaitAll,INFINITE,QS_ALLINPUT);
?
??????????????if?(dRet?==?WAIT_OBJECT_0+?nWaitCount)
?
??????????????{
?
?????????????????????TRACE("收到消息,函數返回值為%d?\n",dRet);
?
?????????????????????while?(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
?
?????????????????????{
?
????????????????????????????TranslateMessage(&msg);
?
????????????????????????????DispatchMessage(&msg);
?
?????????????????????}
?
????????????????????
?
??????????????}
?
??????????????else?if?(dRet?>=?WAIT_OBJECT_0?&&?dRet?<?WAIT_OBJECT_0+?nWaitCount)
?
??????????????{
?
?????????????????????nExitThreadCount++;
?
?????????????????????if?(nExitThreadCount?==?1)
?
?????????????????????{
?
????????????????????????????TRACE("一個線程退出了\n");
?
????????????????????????????int?nIndex=dRet-WAIT_OBJECT_0;
?
????????????????????????????hArray[nIndex]=hArray[nWaitCount-1];
?
????????????????????????????hArray[nWaitCount-1]=NULL;
?
????????????????????????????nWaitCount--;
?
?
?
?????????????????????}
?
?????????????????????else
?
?????????????????????{
?
????????????????????????????TRACE("兩個線程都退出了\n");
?
????????????????????????????break;
?
?????????????????????}
?
??????????????}
?
??????????????else
?
??????????????{
?
?????????????????????DWORD?dErrCode=GetLastError();
?
?????????????????????…
?
?????????????????????break;
?
??????????????}
?
???????}
?
??????
?
}
?
?
?
在上面這個例子中,我將bWaitAll設置為FALSE,目的是當我要等待的兩個線程中由一個退出了,或是有消息到來了,此函數都可以退出。如果我將此參數設置為TRUE,那么,當且僅當我要等待的兩個線程均退出了,這個函數才會返回,這種使用方法有是程序陷入死鎖的危險,故應避免。無論是等待一個還是多個線程,只需將此參數設置為FALSE即可,然后通過函數返回值判斷究竟是那個返回了,還是消息到達了即可。這一要點前面已有陳述,此處再強調一遍。
?
通過函數返回值可以得知究竟哪個線程退出了,當要等待的兩個線程中的一個已經退出后,則應該從新設置等待函數的參數,對等待的句柄數組進行整理。
?
{
?
int?nIndex=dRet-WAIT_OBJECT_0;
?
hArray[nIndex]=hArray[nWaitCount-1];
?
hArray[nWaitCount-1]=NULL;
?
nWaitCount--;
?
}
這組語句就是用來從新設置參數的,其過程就是將等待的總數目減一,并將剛退出的線程的句柄設置為NULL,移到數組的最末位置。
?
上面介紹了線程函數的設計以及在主線程中等待工作線程退出的方法,著重介紹了MsgWaitForMultipleObjects函數的使用要點,希望對大家有所幫助,也希望大家能提寶貴意見,補我之不足,愿與大家共同進步。\
?
?
總結
以上是生活随笔為你收集整理的多线程和MsgWaitForMultipleObjects的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java仿聊天室项目总结_Java团队课
- 下一篇: android x86_64 服务器运行