線程間的同步概述
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
在給定的限制條件下,允許多個進程同時訪問共享資源