在windows系統中線程間的通信一般采用四種方式:全局變量方式、消息傳遞方式、參數傳遞方式和線程同步法。下面分别作介紹: 1.全局變量方式 由于屬于同一個程序的各個線程共享作業系統配置設定該程序的資源,故解決線程間通信最簡單的一種方法是使用全局變量。對于标準類型的全局變量,我們建議使用volatile 修飾符,它告訴編譯器無需對該變量作任何的優化,即無需将它放到一個寄存器中,并且該值可被外部改變。 執行個體示範 該執行個體采用全局變量來控制時間顯示線程的顯示格式,比較簡單。 主要的代碼如下: .h頭檔案 //線程函數聲明 DWORD WINAPIThreadProc(LPVOIDlpParam); protected: HANDLE m_hThread;//線程句柄 DWORD m_nThread;//線程ID .cpp實作檔案 volatileBYTE m_nShowFlag = 31;//定義全局變量,用于控制顯示時間的格式。volatile 修飾符的作用是告訴編譯器無需對該變量作任何的優化,即無需将它放到一個寄存器中。 //建立顯示時間的線程,參數無 m_hThread =CreateThread(NULL,0,ThreadProc,NULL,0,&m_nThread); //線程執行函數,用于實時顯示時間,并按規定格式顯示 DWORD WINAPIThreadProc(LPVOID lpParam) { while(m_nShowFlag) { CTime time; CString strTime,strFormat; time=CTime::GetCurrentTime(); strFormat = "%H:%M"; if (m_nShowFlag&2) {//日期 strFormat = "%Y-%m-%d" + strFormat; } if (m_nShowFlag&4) {//秒鐘 strFormat += ":%S"; } if (m_nShowFlag&8) {//周數 strFormat += "%W"; } if (m_nShowFlag&16) {//星期 strFormat += "%a"; } strTime=time.Format(strFormat); ::SetDlgItemText(AfxGetApp()->m_pMainWnd->m_hWnd,IDC_STATIC_TIME,strTime); Sleep(100); } return 0; } 運作效果:
歡迎大家修改和指正。 注意事項: (1) 全局變量最好放在.CPP檔案的起始處,而不要放在.h頭檔案中,否則将出現重複連結的編譯錯誤。定義全局變量時最好顯式初始化,預設初始值為零。 (2) 注意語句 ::SetDlgItemText(AfxGetMainWnd()->m_hWndIDC_STATIC_TIME,strTime);在VC6.0中可以通過,但在VC2008卻出錯,這是因為在VC2008中不支援AfxGetMainWnd()->m_hWnd來擷取HWND,但可以采AfxGetApp()->m_pMainWnd->m_hWnd來擷取。是以上面的語句更改為: ::SetDlgItemText(AfxGetApp()->m_pMainWnd->m_hWnd,IDC_STATIC_TIME,strTime); (3) 用全局變量方式來實作多線程的通信比較簡單實用,單注意最好不要多個線程對它進行修改,否則将可能出錯,這将在後面會具體講解。 2.參數傳遞方式 該方式是線程通信的官方标準方法,多數情況下,主線程建立子線程并讓其子線程為其完成特定的任務,主線程在建立子線程時,可以通過傳給線程函數的參數和其通信,三類建立線程的函數都支援參數的傳遞(哪三類?看前面的介紹吧!)。所傳遞的參數是一個32位的指針,該指針不但可以指向簡單的資料,而且可以指向結構體或類等複雜的抽象資料類型。 執行個體示範 下面将分别簡單示範三類建立線程時提供參數傳遞的方法。 主要的代碼如下: .h頭檔案 //線程函數聲明 DWORD WINAPI ThreadFunc1(LPVOID lpParam);//線程函數 void ThreadFunc2(void *pArg); //線程函數 UINT ThreadFunc3(LPVOID lpParam);//線程函數 //全局函數 POINT GetRandPoint();//得到随機點的坐标 //結構體定義,用于向線程傳遞多個參數 struct threadInfo { HWND hWnd;//主視窗句柄 COLORREF clrPen;//畫筆顔色 }; .cpp實作檔案 //開始建立線程:建立個線程 void CMultThreadComm2Dlg::OnStart(void) { //線程:使用win32 API 建立:實時顯示時間 m_hThread1 = CreateThread(NULL,0,ThreadFunc1,&m_stTime.m_hWnd,0,NULL);// //線程:使用CRT 建立:随機畫線 m_info.hWnd = m_hWnd; m_info.clrPen =RGB(255,0,0); _beginthread(ThreadFunc2,0,&m_info); //線程:使用MFC線程函數建立:顯示進度 m_pThread = AfxBeginThread(ThreadFunc3,&m_ctrlProgress); } //停止/開始 void CMultThreadComm2Dlg::OnBnClickedButton1() { // TODO: 在此添加控件通知處理程式代碼 CString szTitle; GetDlgItemText(IDC_BUTTON1,szTitle); if (szTitle == "停止") {//停止 g_bRun = false; SetDlgItemText(IDC_BUTTON1,"開始"); } else {//開始 g_bRun = true; OnStart(); SetDlgItemText(IDC_BUTTON1,"停止"); } } //線程執行函數:實時顯示目前的時間 DWORD WINAPI ThreadFunc1(LPVOID lpParam) { HWND *hWnd = (HWND*)lpParam; //CWnd *pWnd = AfxGetApp()->m_pMainWnd;//當沒有傳遞參數時,可以用該api實作 CWnd *pWnd = CWnd::FromHandle(*hWnd); char tmpbuf[128] ={'0'}; time_t t; while(g_bRun) { t = time(NULL); strftime(tmpbuf,128,"%Y-%m-%d%a %I:%M:%S %p",localtime(&t)); pWnd->SetWindowText(tmpbuf); Sleep(500); } return 0; } //線程執行函數:随機畫線 void ThreadFunc2(void *pArg) { threadInfo *threadinfo= (threadInfo*)pArg; CWnd *pWnd = CWnd::FromHandle(threadinfo->hWnd); CDC *pDC = pWnd->GetDC(); CPen pen(PS_SOLID,2,threadinfo->clrPen); pDC->SelectObject(&pen); pDC->SetROP2(R2_NOTXORPEN); while(g_bRun) { POINT StartPos= GetRandPoint(); POINT EndPos = GetRandPoint(); // CString str; str.Format("%d,%d : %d,%d\n",StartPos.x,StartPos.y,EndPos.x,EndPos.y); TRACE(str); pDC->MoveTo(StartPos); pDC->LineTo(EndPos); Sleep(100); pDC->MoveTo(StartPos); pDC->LineTo(EndPos); Sleep(100); } DeleteObject(pDC); } //線程執行函數:顯示進度 UINT ThreadFunc3(LPVOIDlpParam) { CProgressCtrl *pProgress= (CProgressCtrl*)lpParam; while(g_bRun) { pProgress->StepIt(); Sleep(500); } return 0; } //得到随機點 POINT GetRandPoint() { POINT Point; Point.x = rand()%439; Point.y = rand()%208; return Point; } 運作效果:
工程源碼 下載下傳位址: http://download.csdn.net/detail/cbnotes/4984274 歡迎大家修改和指正。 注意事項: (1) 注意三類線程的建立方法和各自的線程函數的格式(傳回類型各不一樣)。 (2) 注意三類參數的傳遞:單個參數、多參數(結構體),複雜參數(類)。 (3) 采用參數傳遞方式進行線程間的通信隻适用于主線程向從線程的通信。 (4) 不知道大家看出該程式的一個BUG沒有(大家可以下載下傳工程 源碼,編譯并運作,可以很明顯的發現。),就是線程二的随機畫線線程,原意是随機畫線并清除,但運作發現第一次畫線時總是沒有被清除掉,為什麼?望大家動腦,知道的可以留言,大家一起學習和讨論! 3.消息傳遞方式 在Windows程式設計中,應用程式的每一個線程都擁有自己的消息隊列,甚至工作線程也不例外,這樣一來,就使得線程之間利用消息來傳遞資訊就變的非常簡單。我們可以在一個線程的執行函數中向另一個線程發送自定義的消息來達到通信的目的。一個線程向另外一個線程發送消息是通過作業系統實作的。利用Windows作業系統的消息驅動機制,當一個線程發出一條消息時,作業系統首先接收到該消息,然後把該消息轉發給目标線程,接收消息的線程必須已經建立了消息循環。該方式可以實作任意線程間的通信,是以是比較常見和通用的方式。 系統也提供了線程間發送消息的專用函數:PostThreadMessage()。使用者先定義一個使用者消息,在一個線程調用PostThreadMessage()函數,在消息接收線程響應該消息,大體和常用的小子響應處理差不多。隻是消息映射為ON_THREAD_MESSAGE而不是ON_MESSAGE 如果線程有窗體,還可以使用通用的消息發送函數PostMessage()和SendMessage()這兩個函數。注意這兩則的差別,PostMessage()是異步函數,調用後函數立即傳回,而SendMessage()是同步函數,要等相應的消息響應完成後才傳回。 關于PostThreadMessage()、PostMessage()和SendMessage()函數介紹請參考MSDN,在此就不多吃一舉了。 對于PostThreadMessage()的用法: 先定義一個使用者消息,如: #define WM_THREADMSG WMUSER+100 在需要發送消息的線程調用PostThreadMessage()函數,如: ::PostThreadMessage(idThread,WM_THREADMSG,parm1, parm2); 或者 m_pThread->PostThreadMessage(WM_THREADMSG,parm1, parm2); 其中: idThread為消息接受線程的ID,parm1, parm2為要傳遞的參數,m_pThread消息接受線程指針。 在接受消息線程中(或消息處理線程中),先定義消息響應函數,如: afx_msg void OnThreadMessage(WPARAM wParam,LPARAM lParam); 然後在消息映射表中添加該消息的映射,如: ON_THREAD_MESSAGE(WM_THREADMSG,OnThreadMessage) 最後,實作該消息函數,如: //顯示消息處理函數 void XXXXThread::OnThreadMessage(WPARAM wParam,LPARAM lParam) { ;//消息處理 } / 對于PostMessage()/SendMessage()的用法:和上面的差不多,略有不同。 首先也是先定義一個使用者消息,如: #define WM_THREADMSG WMUSER+100 在需要發送消息的線程調用PostMessage()/SendMessage()函數,如: ::PostMessage(hWnd, WM_THREADMSG, parm1, parm2);//發送消息 或者 PWnd->PostMessage(WM_THREADMSG, parm1, parm2); 或者 ::SendMessage(hWnd, WM_THREADMSG, parm1, parm2);//發送消息 或者 PWnd->SendMessage(WM_THREADMSG, parm1, parm2); 其中: hWnd為接受消息的視窗句柄,parm1, parm2為要傳遞的參數,pWnd消息接受視窗指針。 在接受消息線程中(或消息處理線程中),先定義消息響應函數,如: afx_msg LRESULT OnMyMessage(WPARAM wParam,LPARAM lParam); 然後在消息映射表中添加該消息的映射,如: ON_MESSAGE(WM_THREADMSG, OnMyMessage) 最後,實作該消息函數,如: //消息處理函數 LRESULT XXXXWnd::OnMyMessage(WPARAM wParam,LPARAM lParam) { ;//消息處理 return 0; } 執行個體示範 該執行個體主要時計算正整數1-N的累加,并采用單獨的線程來計算累積,并建立另一個使用者界面線程來實時顯示累加的進度,其中涉及到一些線程間的通信,該執行個體主要采用了消息的方式,并結合前面已經介紹的兩種通信方式,比較具有示範和學習的價值。 主要源碼: 主線程頭檔案: DWORD WINAPI ThreadFunc(LPVOIDlpParam);//線程函數 protected: HANDLE m_hThread;//線程句柄 CProgressThread *m_pThread;//使用者界面線程句柄 DWORD m_nThreadID;//線程ID号 主線程實作檔案: //開始計算 void CMultThreadComm3Dlg::OnBnClickedButton1() { // TODO: 在此添加控件通知處理程式代碼 UpdateData(TRUE);//更新資料 g_bStop = false; //重置 m_hThread = CreateThread(NULL,0,ThreadFunc,&m_nRange,0,NULL);//建立計算線程 GetDlgItem( IDC_BUTTON1)->EnableWindow(FALSE);//防止重複計算 SetDlgItemText(IDC_STATIC_RESULT,"計算中..."); } //結果顯示:兩種結果:,正常,,被中途中止 LRESULT CMultThreadComm3Dlg::OnResult(WPARAMwParam,LPARAMlParam) { if (wParam == 1) {//被中途中止 SetDlgItemText(IDC_STATIC_RESULT,"被中止啦!"); } else {//正常結束 SetDlgItemInt(IDC_STATIC_RESULT,lParam); } GetDlgItem(IDC_BUTTON1)->EnableWindow(TRUE);//使能下次計算按鈕 CloseHandle(m_hThread);//關閉線程句柄,注意一定要關閉它 return 0; } //線程執行函數:計算 DWORD WINAPI ThreadFunc(LPVOIDlpParam) { UINT *pRange =(UINT*)lpParam;//得到參數值 long nResult= 0L;//計算結果值 bool bStop = false;//辨別是否中途被中止 CProgressThread *m_pThread= (CProgressThread*)AfxBeginThread(RUNTIME_CLASS(CProgressThread));//建立顯示程序 m_pThread->PostThreadMessage(WM_PROGRESS,0,*pRange);//傳遞參數,進度條的範圍 for(int i=0;i<=*pRange;i++)//開始計算 { if (g_bStop) {//中途取消了 bStop = true; break;//退出循環 } nResult += i; m_pThread->PostThreadMessage(WM_PROGRESS,1,i);//進度 Sleep(10);//為了示範效果,特意延時 } //完成 ::PostMessage(AfxGetApp()->m_pMainWnd->m_hWnd,WM_RESULT,bStop,nResult);//顯示結果 m_pThread->PostThreadMessage(WM_PROGRESS,2,0);//結束進度 return 0; } 累加線程頭檔案: CProgressDlg *m_pProgressDlg;//進度條對話框 unsigned int m_nRange;//進度條的範圍 累加線程實作檔案: BOOL CProgressThread::InitInstance() { // TODO: 在此執行任意逐線程初始化 //建立非模式進度顯示對話框 m_pProgressDlg = newCProgressDlg(); m_pProgressDlg->Create(IDD_DIALOG1); m_pProgressDlg->ShowWindow(SW_SHOW); return TRUE; } BEGIN_MESSAGE_MAP(CProgressThread, CWinThread) ON_THREAD_MESSAGE(WM_PROGRESS, &CProgressThread::OnThreadMsg) END_MESSAGE_MAP() // CProgressThread 消息處理程式 //線程消息處理函數 void CProgressThread::OnThreadMsg(WPARAM wParam,LPARAM lParam) { if (wParam == 0) {//初始化進度條 m_nRange = lParam; m_pProgressDlg->m_ProgressCtrl.SetRange(0,lParam); } else if (wParam == 1) {//顯示進度 m_pProgressDlg->m_ProgressCtrl.SetPos(lParam);//顯示進度 CString str; str.Format("%d%%",int((float)lParam/m_nRange*100)); m_pProgressDlg->m_stValue.SetWindowText(str);//顯示百分數 str.Format("計算進行中,請稍等... %d/%d cbNotes",lParam,m_nRange);//顯示實時的目前操作 m_pProgressDlg->SetWindowText(str); } else {//完成,退出進度條 m_pProgressDlg->CloseDlg();//結束程序對話框 AfxEndThread(0);//終止本線程,也可以使用PostQuitMessage(0); } } 進度條對話框類 //系統消息處理 void CProgressDlg::OnSysCommand(UINT nID, LPARAM lParam) { // TODO: 在此添加消息處理程式代碼和/或調用預設值 if (nID == SC_CLOSE)//關閉 {//攔截關閉按鈕消息 if (AfxMessageBox("确定要【終止】本次計算嗎?",MB_YESNO|MB_APPLMODAL|MB_ICONQUESTION|MB_DEFBUTTON2)==IDYES) { g_bStop = true;//中途取消操作,結束計算線程。 } return; } CDialog::OnSysCommand(nID, lParam); } //關閉視窗 void CProgressDlg::CloseDlg(void) { OnCancel(); } //重載OnCancel():注意非模式對話框的退出 void CProgressDlg::OnCancel() { // TODO: 在此添加專用代碼和/或調用基類 DestroyWindow();// //CDialog::OnCancel(); } void CProgressDlg::PostNcDestroy() { // TODO: 在此添加專用代碼和/或調用基類 CDialog::PostNcDestroy(); delete this; } 運作結果:
工程源碼下載下傳位址: http://download.csdn.net/detail/cbnotes/5007011 歡迎大家修改和指正。 注意事項: (1) 注意線程消息的映射方式: ON_THREAD_MESSAGE(WM_PROGRESS, &CProgressThread::OnThreadMsg) 和一般的視窗消息映射不同,不要搞錯了。 (2) 注意使用者界面線程的退出,計算完後要記得結束該線程,有專門的函數: AfxEndThread(0),也可以使用PostQuitMessage(0)。這兩種方式都是比較安全的。不推薦強制中止線程,否則會出現意想不到的問題。 (3) 該執行個體還實作了執行過程按退出按鈕時的處理方法,采用了全局變量的 方式來控制計算線程的中途退出,我自認為是一種比較好的控制方式。我開始采用強制中止的方式會出現消息延後的現象(就是在退出按鈕的處理函數裡向主線程發送消息postmessage),一直沒有找到原因,有興趣的朋友可以試試其它的退出方式供大家讨論學習。 (4) 注意非模式對話框退出的處理方法,和模式對話框的退出不一樣的。 (5)用CreateThread()函數建立線程将傳回一個線程句柄,通過該句柄你可以控制和操作該線程,當你不用時可以一建立該線程後就關閉該句柄,有專門的函數CloseHandle()。關閉句柄并代表關閉線程,隻是你不能在外部控制該線程(比如,提前結束,更改優先級等)。線上程結束後,系統将自動清理線程資源,但并不自動關閉該句柄,是以線程結束後要記得關閉該句柄,你可能要問為什麼你這麼強調要關閉該句柄,是因為該句柄(HANDLE)是核心對象,核心對象的管理是作業系統管理的,具體你可以查查相關資料,我在此就不啰嗦了。 4.線程同步法 還可以通過線程同步來實作線程間通信。例如有兩個線程,線程A寫入資料,線程B讀出線程A準備好的資料并進行一些操作。這種情況下,隻有當線程A寫好資料後線程B才能讀出,隻有線程B讀出資料後線程A才能繼續寫入資料,這兩個線程之間需要同步進行通信。關于線程同步的方法和控制是編寫多線程的核心和難點,方法也比較多,在此就不具體講解和執行個體示範。