線程間的同步概述 1.前言 前面幾篇文章著重介紹了多線程的三種創建方式及多線程間的4種通信方式,並采用大量的實例演示,相信大家對線程的創建和使用有了一定的了解。若還不了解請復習下前面的文章,多動手寫代碼和調試,光看不練,假把式。 今天先請大家看看下面一個多線程程序,操作很簡單,就是創建9個線程,並輸出相應的線程編號(即報數)。主要代碼如下: [cpp] //聲明線程處理函數 <strong><span style="color:#ff0000;">unsigned __stdcall</span></strong>ThreadFunc( void* pArguments);//工作線程函數 HANDLE m_handle[9];//線程句柄列表 CListBox m_List; //數據列表控件 ///////////////////////////////////////////////// int g_nCount= 0;//這個是<strong><span style="color:#ff0000;">全局變量</span></strong>,用於線程報數(計數) //演示開始:創建線程 void CThreadProblem1Dlg::OnBnClickedButton1() { // TODO: 在此添加控件通知處理程序代碼 GetDlgItem(IDC_BUTTON1)->EnableWindow(FALSE); m_List.ResetContent();//清空列表 g_nCount = 0; //重置報數, SetDlgItemInt(IDC_EDIT_NUM,++m_nNum); //顯示操作的次數 //創建多線程 for (int i=0;i<9;i++) { m_handle[i] = (HANDLE)<strong><span style="color:#ff0000;">_beginthreadex</span></strong>(NULL,0, ThreadFunc,&m_List,0, NULL); } //WaitForMultipleObjects(10, handle, TRUE, INFINITE); //在此處等待退出,將發現程序假死了。所以采用線程的方式等待 _beginthreadex(NULL,0, WaitThread,<strong><span style="color:#ff0000;">this</span></strong>, 0, NULL); //等待上述的個線程都退出 } //工作線程函數 unsigned __stdcall ThreadFunc(void* pArguments) { Sleep(100);//相關處理 g_nCount++; //計數加 CListBox *pList= (CListBox*)pArguments; CString str; str.Format(" 子線程ID號為%4d 報數為:%d",GetCurrentThreadId(),g_nCount); pList->AddString(str);//輸出 Sleep(100);//相關處理 return 0; } //線程函數:等待個演示線程都退出再使能開始按鈕 unsigned __stdcall WaitThread(void* pArguments) { CThreadProblem1Dlg *pMainDlg= (CThreadProblem1Dlg *)pArguments; <strong><span style="color:#ff0000;">WaitForMultipleObjects</span></strong>(9,pMainDlg->m_handle,TRUE, INFINITE); //等待所有線程都結束 EnableWindow(GetDlgItem(AfxGetApp()->m_pMainWnd->m_hWnd,IDC_BUTTON1),TRUE);//使能開始按鈕 return 0; } 當運行一次OnBnClickedButton1()函數,將顯示下面的結果: 你一看,沒錯呀,就應該是這樣的,沒有錯呀!多運行幾次也是這樣的。但我要肯定的告訴你,上面的程序是有嚴重的問題,而運行結果也欺騙了你。正是運行結果大大蒙騙了你的理智和大腦。你發現問題了嗎?(提示:不是報數順序的問題)www.2cto.com 正是該錯誤有隱蔽性,你很難從結果中發現問題,除非你運氣特別好,一運行就能重現問題,但作為程序員,你決不能僅靠運氣,不可能你每次的運氣都這麼好。 多運行幾次上面的程序,你有可能發現問題,現在我把該程序改進下,使其具有自動識別錯誤的智能,你一眼就能發現問題的。 改進點:添加結果檢測功能,若正常,其線程的報數應該為1-9,有可能順序有變,但總和為45=1+2+3+4+5+6+7+8+9。程序將一直循環到程序退出。若不等於45就退出循環,表示有問題,即讓程序一直運行,直到有錯誤為止。前面的OnBnClickedButton1()函數和ThreadFunc()函數保存不變,WaitThread()函數添加一個判斷語句,改進程序如下: [cpp] //線程函數:等待個演示線程都退出再使能開始按鈕 unsigned __stdcall WaitThread(void* pArguments) { CThreadProblem1Dlg *pMainDlg= (CThreadProblem1Dlg *)pArguments; WaitForMultipleObjects(9, pMainDlg->m_handle, TRUE,INFINITE); //等待所有線程都結束 EnableWindow(GetDlgItem(AfxGetApp()->m_pMainWnd->m_hWnd,IDC_BUTTON1),TRUE);//使能開始按鈕 <span style="color:#ff0000;"><strong> if(pMainDlg->m_bAuto&& !pMainDlg->IsError()) {//若自動使能,則繼續下輪操作 pMainDlg->OnBnClickedButton1(); }</strong></span> return 0; } //添加IsError()函數,用以判斷結果是否正確。 // 自動判斷每次運行結果是否正確 bool CThreadProblem1Dlg::IsError(void) { int nValue[9]={0}; int nResult = 0; CString szText; for(int i=0;i<9;i++) {//得到各個線程的報數 m_List.GetText(i,szText); szText = szText.Right(1); nValue[i] = atoi(szText); nResult += nValue[i]; } //判斷是否有相同的值出現 if (nResult != <span style="color:#ff0000;"><strong>45</strong></span>) {//有錯誤 return true; } return false; } 再運行上面的程序,選中“自動判斷”,程序將很快不停的運行,但很快將又停下來,運行結果如下圖所示,有可能你的結果和我的不一樣,但類型差不多的。 現在你發現問題了嗎?對了,報數出現相同數了(見上圖出現兩個“2”)。 你可能要問,怎麼會這樣呢? 這就是多線程最容易出現的問題,也是多線程編程的難點和核心。再說說上面程序,創建了9個線程,這9個線程是同時運行的(即並行運行),它們都要修改變量全局g_nCount(g_nCount++;),就有可能兩個或多個線程同時讀取到g_nCount,而當前的g_nCount已經被其它線程修改,即輸出的不是線程當前的值。這和單線程的順序執行是有很大不同的。 那有什麼方法解決上面的問題嗎?當然有,這就是在江湖中大名鼎鼎的線程同步技術,而且系統提供了多種線程同步的技術/方法。 2.什麼是同步 “同步”不是指平常所說的兩件事情同時進行。它的目的是使多個線程之間協調工作,而且常常是避免兩個線程同時進行某些操作,比如同時訪問同一個共享資源。一般來說,同步是通過暫時將會發生沖突操作的某個線程暫停執行(稱為阻塞線程),然後等待不會沖突時再繼續執行。 3.需要同步的情況 3.1、多個線程同時訪問同一對象時 MFC對象在對象級不是線程安全的,只有在類級才是。如:兩個線程可以安全地使用兩個不同的CString對象,但同時使用同一個CString對象就可能產生問題。如果必須使用同一個對象,那麼應該采取適當的同步化措施。 3.2、多個線程之間需要協調運行 例如,如果第二個線程需要等待第一個線程完成到某一步時才能運行,那麼該線程應該暫時掛起以減少對CPU的占用時間,提高程序的執行效率。當第一個線程完成了相應的步驟後,應該發出某種信號來激活第二個線程。 4.Windows中的4種線程同步技術 4.1、Events(事件)——CEvent 作為標志在線程之間傳遞信號。簡單地說,類似一個布爾型變量的開關作用。 4.2、Critical Sections(臨界段)——CCriticalSection 在進程中作為關鍵字以獲得對共享資源的訪問 4.3、Mutexes(互斥量)——CMutex 與臨界段的工作方式相似,只是該對象可以用於多進程中的線程同步,而不是用於單進程中 4.4、Semaphores(信號量)——CSemaphore 在給定的限制條件下,允許多個進程同時訪問共享資源