使用多線程技術(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ù)操作,卻可以自如的調(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)驗的人都有體會,多線程設(shè)計最重要的就是要處理好線程間的同步和通訊問題。如解決不好這個問題,會給程序帶來潛藏的隱患。線程的同步可以利用臨界區(qū)、事件、互斥體和信號量來實現(xiàn),線程間的通訊可利用全局變量和發(fā)消息的形式實現(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)用事件變量的典型方式 ,也是推薦的方式。
無論是工作線程還是用戶界面線程,都有消息隊列,都可以接收別的線程發(fā)過來的消息也可以給別的線程發(fā)送消息。給工作線程發(fā)消息使用的函數(shù)是PostThreadMessage() 。此函數(shù)的第一個參數(shù)是接收消息的線程的ID。此函數(shù)是異步執(zhí)行的,機(jī)制和PostMessage一樣,就是把消息拋出后就立即返回,不理會消息是否被處理完了。
這里還有著重強調(diào)一點,線程消息隊列是操作系統(tǒng)幫我們維護(hù)的一種資源,所以它的容量也是有限制的。 筆者曾經(jīng)做過實驗,在5~6秒事件內(nèi)調(diào)用PostThreadMessage往線程消息隊列里發(fā)送5萬多條消息,可是由于線程函數(shù)處理消息的速度遠(yuǎn)慢于發(fā)送速度,結(jié)果導(dǎo)致線程消息隊列里已經(jīng)堆滿了消息,而發(fā)送端還在發(fā)消息,最終導(dǎo)致消息隊列溢出,很多消息都丟失了。所以,如果你要在短時間內(nèi)往線程消息隊列里發(fā)送很多條消息,那就要判斷一下PostThreadMessage函數(shù)的返回值。當(dāng)消息隊列已經(jīng)溢出時,此函數(shù)返回一個錯誤值。根據(jù)返回值,你就可以控制是否繼續(xù)發(fā)送。
工作線程給主線程發(fā)消息使用的是SendMessage和PostMessage函數(shù)。這兩個函數(shù)的區(qū)別在于SendMessage函數(shù)是阻塞方式,而 PostMessage函數(shù)是非阻塞方式。如果不是嚴(yán)格要求工作線程與主線程必須同步執(zhí)行,則推薦使用PostMessage。不要在線程函數(shù)體內(nèi)操作 MFC控件,因為每個線程都有自己的線程模塊狀態(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ù)可以從消息隊列中抓取消息,當(dāng)抓取到消息后,GetMessage函數(shù)會將此條消息從消息隊列中刪除。而且,如果消息隊列中沒有消息,則GetMessage函數(shù)不會返回,CPU轉(zhuǎn)而回去執(zhí)行別的線程,釋放控制權(quán)。GetMessage返回的條件是抓取的消息是WM_QUIT。
PeekMessage函數(shù)也可以從消息隊列中抓取消息,如果它的最后一個參數(shù)設(shè)置為PM_NOREMOVE,則不從消息隊列中刪除此條消息,此條消息會一直保留在消息隊列中。如果它的最后一個參數(shù)是PM_REMOVE,則會刪除此條消息。如果消息隊列中沒有消息,則PeekMessage函數(shù)會立刻返回,而不是像GetMessage一樣就那樣等在那兒。PeekMessage函數(shù)就像是窺探一下消息隊列,看看有沒有消息,有的話就處理,沒有就離開了。這一點也是兩個函數(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ā)消息,因為這個函數(shù)是同步操作,就是如果SendMessage函數(shù)不執(zhí)行完,是不會返回的,這樣線程函數(shù)就無法繼續(xù)執(zhí)行。有時這種操作容易導(dǎo)致工作線程和主線程死鎖,這個我們后面會談到,會介紹一種解決方法。
三 線程的退出
線程的退出有多種方式,比如可以調(diào)用TerminateThread()函數(shù)強制線程退出,但不推薦這種方式,因為這樣做會導(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)核對象,而且線程對象的特點是當(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ī)會去消息隊列中抓取消息并處理它,結(jié)果導(dǎo)致SetPos函數(shù)不會返回,工作線程也被掛起,典型的死鎖。如果不用m_progress.SetPos,而改用 this->SendMessage(…),其結(jié)果是一樣的。此時如果用了PostMessage,則工作線程會順利退出,因為 PostMessage是異步執(zhí)行的。由此可見,在主線程中用WaitForSingleObject等待工作線程退出是有很大隱患的。
為解決這一問題,微軟特提供了一個MsgWaitForMultipleObjects函數(shù),該函數(shù)的特點是它不但可以等待內(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:等待的事件,單位是毫秒??梢栽O(shè)置為INFINITE,無窮等待
DWORD dwWakeMask:等待的消息類型,通??梢栽O(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ù)由于消息到來而返回,則需要用戶主動去消息隊列中將消息抓取出來,然后派發(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à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ù)的使用要點,希望對大家有所幫助,也希望大家能提寶貴意見,補我之不足,愿與大家共同進(jìn)步。