线程函数的设计以及MsgWaitForMultipleObjects函数的使用要点
使用多線程技術(shù)可以顯著地提高程序性能,本文就講講在程序中如何使用工作線程,以及工作線程與主線程通訊的問題。
?
一 創(chuàng)建線程
?
?????? 使用MFC提供的全局函數(shù)AfxBeginThread()即可創(chuàng)建一個工作線程。線程函數(shù)的標(biāo)準(zhǔn)形式為 UINT?MyFunProc(LPVOID );此函數(shù)既可以是全局函數(shù),也可以是類的靜態(tài)成員函數(shù)。之所以必須是靜態(tài)成員函數(shù),是由于類的非靜態(tài)成員函數(shù),編譯器在編譯時會自動加上一個this指針參數(shù),如果將函數(shù)設(shè)置為靜態(tài)的成員函數(shù),則可以消除this指針參數(shù)。如果想在線程函數(shù)中任意調(diào)用類的成員變量(此處指的是數(shù)據(jù)成員,而不是控件關(guān)聯(lián)的成員變量),則可以將類的指針作為參數(shù)傳遞給線程函數(shù),然后經(jīng)由該指針,就可以調(diào)用類的成員變量了。
//線程函數(shù),類的靜態(tài)成員函數(shù)
UINT CThreadTest::TH_SetProgress(LPVOID lpVoid)
{
?????? CThreadTest *pTest=(CThreadTest *)lpVoid;
?????? pTest->SetProgress();
?????? return 0;
}
//類的成員函數(shù),此函數(shù)執(zhí)行實(shí)際的線程函數(shù)操作,卻可以自如的調(diào)用成員數(shù)據(jù)
void CThreadTest::SetProgress()
{
int nCount=0;
?????? while (1)
?????? {
????????????? m_progress.SetPos(nCount); //設(shè)置進(jìn)度條進(jìn)度
//??????????? this->SendMessage(WM_SETPROGRESSPOS,nCount,0);//也可以采用這種方式設(shè)置
????????????? nCount++;
????????????? if (g_exitThread)
????????????? {
???????????????????? return;
????????????? }
????????????? Sleep(200);
?????? }
}
?
二 線程函數(shù)體的設(shè)計
?
有過多線程設(shè)計經(jīng)驗(yàn)的人都有體會,多線程設(shè)計最重要的就是要處理好線程間的同步和通訊問題。如解決不好這個問題,會給程序帶來潛藏的隱患。線程的同步可以利用臨界區(qū)、事件、互斥體和信號量來實(shí)現(xiàn),線程間的通訊可利用全局變量和發(fā)消息的形式實(shí)現(xiàn)。其中事件和臨界區(qū)是使用得比較多的工具。請看下面的線程函數(shù)體:
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)
???????????????????? {
??????????????????????????? //繼續(xù)分析
??????????????????????????? //
???????????????????? }
????????????? }
?????? }
?
?????? return 0;
}
?
上面的線程函數(shù)用到了三個事件變量eventStartAnalyse、eventExitAnalyse和eventPause,分別用來控制線程函數(shù)的啟動、退出以及暫停。再配以WaitForSingleObject函數(shù),就可以自如的控制線程函數(shù)的執(zhí)行,這是在線程函數(shù)體內(nèi)應(yīng)用事件變量的典型方式,也是推薦的方式。
無論是工作線程還是用戶界面線程,都有消息隊(duì)列,都可以接收別的線程發(fā)過來的消息也可以給別的線程發(fā)送消息。給工作線程發(fā)消息使用的函數(shù)是PostThreadMessage()。此函數(shù)的第一個參數(shù)是接收消息的線程的ID。此函數(shù)是異步執(zhí)行的,機(jī)制和PostMessage一樣,就是把消息拋出后就立即返回,不理會消息是否被處理完了。
這里還有著重強(qiáng)調(diào)一點(diǎn),線程消息隊(duì)列是操作系統(tǒng)幫我們維護(hù)的一種資源,所以它的容量也是有限制的。筆者曾經(jīng)做過實(shí)驗(yàn),在5~6秒事件內(nèi)調(diào)用PostThreadMessage往線程消息隊(duì)列里發(fā)送5萬多條消息,可是由于線程函數(shù)處理消息的速度遠(yuǎn)慢于發(fā)送速度,結(jié)果導(dǎo)致線程消息隊(duì)列里已經(jīng)堆滿了消息,而發(fā)送端還在發(fā)消息,最終導(dǎo)致消息隊(duì)列溢出,很多消息都丟失了。所以,如果你要在短時間內(nèi)往線程消息隊(duì)列里發(fā)送很多條消息,那就要判斷一下PostThreadMessage函數(shù)的返回值。當(dāng)消息隊(duì)列已經(jīng)溢出時,此函數(shù)返回一個錯誤值。根據(jù)返回值,你就可以控制是否繼續(xù)發(fā)送。
工作線程給主線程發(fā)消息使用的是SendMessage和PoseMessage函數(shù)。這兩個函數(shù)的區(qū)別在于SendMessage函數(shù)是阻塞方式,而PoseMessage函數(shù)是非阻塞方式。如果不是嚴(yán)格要求工作線程與主線程必須同步執(zhí)行,則推薦使用PoseMessage。不要在線程函數(shù)體內(nèi)操作MFC控件,因?yàn)槊總€線程都有自己的線程模塊狀態(tài)映射表,在一個線程中操作另一個線程中創(chuàng)建的MFC對象,會帶來意想不到的問題。更不要在線程函數(shù)里,直接調(diào)用UpdataData()函數(shù)更新用戶界面,這會導(dǎo)致程序直接crash。而應(yīng)該通過發(fā)送消息給主線程的方式,在主線程的消息響應(yīng)函數(shù)里操作控件。上面提到的SetProgress函數(shù)和AnalyseProc函數(shù)均為線程函數(shù),但它們都不能接收別的線程發(fā)過來的消息,雖然它們都可以給主線程發(fā)消息。它們要想能夠接收別的線程發(fā)過來的消息,則必須調(diào)用GetMessage或PeekMessage函數(shù)。這兩個函數(shù)的主要區(qū)別在于:
GetMessage函數(shù)可以從消息隊(duì)列中抓取消息,當(dāng)抓取到消息后,GetMessage函數(shù)會將此條消息從消息隊(duì)列中刪除。而且,如果消息隊(duì)列中沒有消息,則GetMessage函數(shù)不會返回,CPU轉(zhuǎn)而回去執(zhí)行別的線程,釋放控制權(quán)。GetMessage返回的條件是抓取的消息是WM_QUIT。
PeekMessage函數(shù)也可以從消息隊(duì)列中抓取消息,如果它的最后一個參數(shù)設(shè)置為PM_NOREMOVE,則不從消息隊(duì)列中刪除此條消息,此條消息會一直保留在消息隊(duì)列中。如果它的最后一個參數(shù)是PM_REMOVE,則會刪除此條消息。如果消息隊(duì)列中沒有消息,則PeekMessage函數(shù)會立刻返回,而不是像GetMessage一樣就那樣等在那兒。PeekMessage函數(shù)就像是窺探一下消息隊(duì)列,看看有沒有消息,有的話就處理,沒有就離開了。這一點(diǎn)也是兩個函數(shù)的最大不同。下面的代碼演示了在線程函數(shù)中使用這兩個函數(shù)的三種方式,這三種方法可以達(dá)到同樣的效果:
void CThreadTest::SetSlider()
{
?
//?在線程函數(shù)里啟動一個時鐘,每50毫秒發(fā)送一個WM_TIMER消息
?????? int nTimerID=::SetTimer(NULL,1,50,NULL);
?
?????? int nSliderPos=0;
?
?????? MSG msg;
?????? while (1)
?????? {
//方式一??? 使用GetMessage函數(shù)??
/*?????????? 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函數(shù)??
?
/*?????????? 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
????????????? {
?????????????????????? //必須有此操作,要不然當(dāng)沒有消息到來時,線程函數(shù)相當(dāng)于陷
//入空循環(huán),cpu的占有率會飆升
???????????????????? Sleep(20);
????????????? }
*/
?
//方式三?? 同時使用PeekMessage和GetMessage函數(shù)??
?
????????????? 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);
????????????? }
?????? }
}
前面已經(jīng)介紹過了,不建議線程函數(shù)里用SendMessage給主線程發(fā)消息,因?yàn)檫@個函數(shù)是同步操作,就是如果SendMessage函數(shù)不執(zhí)行完,是不會返回的,這樣線程函數(shù)就無法繼續(xù)執(zhí)行。有時這種操作容易導(dǎo)致工作線程和主線程死鎖,這個我們后面會談到,會介紹一種解決方法。
?
三 線程的退出
?
線程的退出有多種方式,比如可以調(diào)用TerminateThread()函數(shù)強(qiáng)制線程退出,但不推薦這種方式,因?yàn)檫@樣做會導(dǎo)致線程中的資源來不及釋放。最好的也是推薦的方式,是讓線程函數(shù)自己退出。就像上面介紹的SetProgress()函數(shù)中,用全局變量g_exitThread使線程退出。
而AnalyseProc用WAIT_OBJECT_0 ==WaitForSingleObject(m_eventExitAnalyse.m_hThread,0)這種方式來退出線程,還有在SetSlider函數(shù)中利用發(fā)送自定義消息WM_QUIT_THREAD的方式令線程退出。這些都是可以使用的方法。
?????? 當(dāng)主線程要退出時,為了能保證線程的資源能全部地釋放,主線程必須等待工作線程退出。線程對象和進(jìn)程對象一樣,也是內(nèi)核對象,而且線程對象的特點(diǎn)是當(dāng)線程退出時,線程內(nèi)核對象會自動變?yōu)橛行盘枲顟B(tài),能夠喚醒所有正在等待它的線程。我們通常都習(xí)慣于使用WaitForSingleObject等函數(shù)來等待某個內(nèi)核對象變?yōu)橛行盘枲顟B(tài),但是我想說的是,在主線程中不要使用WaitForSingleObject和WaitForMultipleObjects兩個函數(shù)等待線程退出,其原因就是有導(dǎo)致程序死鎖的隱患,特別是線程函數(shù)里調(diào)用了SendMessage或是直接操作了MFC對象,更易出現(xiàn)此種現(xiàn)象。下面的函數(shù)是一個在主線程中用來等待SetProgress()線程函數(shù)退出的函數(shù):
?
//退出線程
void CThreadTest::OnButton2()
{
?????? g_exitThread=TRUE;?//設(shè)置全局變量為真,令線程退出
?
#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????
}
在上面的函數(shù)中我用#if?#else?#endif這組預(yù)編譯指令控制函數(shù)的執(zhí)行代碼,如果我令#if 1,則執(zhí)行WaitForSingleObject函數(shù),如果我令#if 0,則執(zhí)行DWORD dRet路徑。首先令#if ?1,測試會發(fā)現(xiàn),程序死鎖了。原因是當(dāng)程序執(zhí)行到WaitForSingleObject函數(shù)時,主線程掛起,等待線程函數(shù)退出,此時CPU切換到線程函數(shù)體內(nèi)執(zhí)行,如果執(zhí)行到if (g_exitThread)處,則線程函數(shù)順利退出,可如果執(zhí)行到m_progress.SetPos(nCount)處,由于SetPos函數(shù)是在主線程中完成的操作,Windows是基于消息的操作系統(tǒng),很多操作都是靠發(fā)消息完成的,由于主線程已經(jīng)掛起,所以沒有機(jī)會去消息隊(duì)列中抓取消息并處理它,結(jié)果導(dǎo)致SetPos函數(shù)不會返回,工作線程也被掛起,典型的死鎖。如果不用m_progress.SetPos,而改用this->SendMessage(…),其結(jié)果是一樣的。此時如果用了PostMessage,則工作線程會順利退出,因?yàn)镻ostMessage是異步執(zhí)行的。由此可見,在主線程中用WaitForSingleObject等待工作線程退出是有很大隱患的。
?????? 為解決這一問題,微軟特提供了一個MsgWaitForMultipleObjects函數(shù),該函數(shù)的特點(diǎn)是它不但可以等待內(nèi)核對象,還可以等消息。也就是當(dāng)有消息到來時,該函數(shù)也一樣可以返回,并處理消息,這樣就給了工作線程退出的機(jī)會。
DWORD MsgWaitForMultipleObjects( DWORD nCount, //要等待的內(nèi)核對象數(shù)目 LPHANDLE pHandles, //要等待的內(nèi)核對象句柄數(shù)組指針 BOOL fWaitAll, //是等待全部對象還是單個對象 DWORD dwMilliseconds,//等待時間 DWORD dwWakeMask );//等待的消息類型 ? 下面就詳解一下該函數(shù)的參數(shù)使用方法: DWORD nCount:要等待的內(nèi)核對象的數(shù)目。如果等待兩個線程退出,則nCount=2; LPHANDLE pHandles:要等待的內(nèi)核對象句柄數(shù)組指針。 ? 如果只要等待一個線程退出,則直接設(shè)置該線程句柄的指針即可: MsgWaitForMultipleObjects(1,&m_pThread->m_hThread,…) ? 如果要等待兩個線程退出,則使用方法為: HANDLE hArray[2]={ m_pThread1->m_hThread , m_pThread2->m_hThread }; MsgWaitForMultipleObjects(2,hArray,…) ? BOOL fWaitAll: TRUE-表示只有要等待的線程全部退出后,此函數(shù)才返回, ?????????????? FALSE-表示要等待的線程中任意一個退出了,或是有消息到達(dá)了,此函數(shù)均會返回。 在上面的OnButton2()函數(shù)中,我要等待一個線程退出,將fWaitAll設(shè)置為 FALSE,目的是無論是線程真的退出了,還是有消息到達(dá)了,該函數(shù)都能返回。 如果將該fWaitAll設(shè)置為TRUE,那么函數(shù)返回的唯一條件是線程退出了,即便 是有消息到來了,該函數(shù)也一樣不會返回。 ? DWORD dwMilliseconds:等待的事件,單位是毫秒。可以設(shè)置為INFINITE,無 窮等待 ? DWORD dwWakeMask:等待的消息類型,通常可以設(shè)置為QS_ALLINPUT。此宏表示的是可以等待任意類型的消息。當(dāng)然,也可以指定等待的消息類型。 ? #define QS_ALLINPUT??????? (QS_INPUT???????? | \ ??????????????????????????? QS_POSTMESSAGE?? | \ ??????????????????????????? QS_TIMER???????? | \ ??????????????????????????? QS_PAINT???????? | \ ??????????????????????????? QS_HOTKEY??????? | \ ??????????????????????????? QS_SENDMESSAGE) ?返回值:DWORD dRet 通過函數(shù)返回值,可以得到一些有效信息。函數(shù)返回值依fWaitAll設(shè)置的不同而有所不同。下面是函數(shù)返回值的幾種常見類型:
dRet = 0xFFFFFFFF :?? 表示函數(shù)調(diào)用失敗,可用GetLastError()得到具體的出錯信息;
dRet =WAIT_OBJECT_0+nCount:表示有消息到達(dá)了;
?
如果fWaitAll設(shè)置為TRUE
dRet = WAIT_OBJECT_0,表示所有等待的核心對象都激發(fā)了,或是線程都退出了;
如果fWaitAll設(shè)置為FALSE
dRet = WAIT_OBJECT_0 ~ WAIT_OBJECT_0+nCount-1:表示等待的內(nèi)核對象被激發(fā)了,index=dRet - WAIT_OBJECT_0,表示hArray[]數(shù)組中索引為index的那個對象被激發(fā)了。
?
當(dāng)函數(shù)由于消息到來而返回,則需要用戶主動去消息隊(duì)列中將消息抓取出來,然后派發(fā)出去,這樣該消息就會被處理了。其具體的操作就是:
while (PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
?????? TranslateMessage(&msg);
?????? DispatchMessage(&msg);
}
?
下面再看一個用這個函數(shù)等待兩個線程退出的例子:
//關(guān)閉線程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;?????? //標(biāo)記已經(jīng)有幾個線程退出了
?????? BOOL bWaitAll=FALSE;
?????? int?nWaitCount=2;??? //初始等待的線程數(shù)目
?
?????? while (1)
?????? {
????????????? dRet=MsgWaitForMultipleObjects(nWaitCount,hArray,bWaitAll,INFINITE,QS_ALLINPUT);
????????????? if (dRet == WAIT_OBJECT_0+ nWaitCount)
????????????? {
???????????????????? TRACE("收到消息,函數(shù)返回值為%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設(shè)置為FALSE,目的是當(dāng)我要等待的兩個線程中由一個退出了,或是有消息到來了,此函數(shù)都可以退出。如果我將此參數(shù)設(shè)置為TRUE,那么,當(dāng)且僅當(dāng)我要等待的兩個線程均退出了,這個函數(shù)才會返回,這種使用方法有是程序陷入死鎖的危險,故應(yīng)避免。無論是等待一個還是多個線程,只需將此參數(shù)設(shè)置為FALSE即可,然后通過函數(shù)返回值判斷究竟是那個返回了,還是消息到達(dá)了即可。這一要點(diǎn)前面已有陳述,此處再強(qiáng)調(diào)一遍。
通過函數(shù)返回值可以得知究竟哪個線程退出了,當(dāng)要等待的兩個線程中的一個已經(jīng)退出后,則應(yīng)該從新設(shè)置等待函數(shù)的參數(shù),對等待的句柄數(shù)組進(jìn)行整理。
{
int nIndex=dRet-WAIT_OBJECT_0;
hArray[nIndex]=hArray[nWaitCount-1];
hArray[nWaitCount-1]=NULL;
nWaitCount--;
}
這組語句就是用來從新設(shè)置參數(shù)的,其過程就是將等待的總數(shù)目減一,并將剛退出的線程的句柄設(shè)置為NULL,移到數(shù)組的最末位置。
?
上面介紹了線程函數(shù)的設(shè)計以及在主線程中等待工作線程退出的方法,著重介紹了MsgWaitForMultipleObjects函數(shù)的使用要點(diǎn),希望對大家有所幫助,也希望大家能提寶貴意見,補(bǔ)我之不足,愿與大家共同進(jìn)步。
總結(jié)
以上是生活随笔為你收集整理的线程函数的设计以及MsgWaitForMultipleObjects函数的使用要点的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql 视图 mybatis_Myb
- 下一篇: java用户登录窗口怎么删除_从窗口中删