C++多執行緒--執行緒間通訊與執行緒同步
原文地址:http://blog.csdn.net/yanpingsz/article/details/5891693(轉)
http://blog.csdn.net/zjc0888/article/details/7372258 (轉)
執行緒間通訊
一般而言,應用程式中的一個次要執行緒總是為主執行緒執行特定的任務,這樣,主執行緒和次要執行緒間必定有一個資訊傳遞的渠道,也就是主執行緒和次要執行緒間要進行通訊。這種執行緒間的通訊不但是難以避免的,而且在多執行緒程式設計中也是複雜和頻繁的,下面將進行說明。
- 使用全域性變數進行通訊
由於屬於同一個程序的各個執行緒共享作業系統分配該程序的資源,故解決執行緒間通訊最簡單的一種方法是使用全域性變數。對於標準型別的全域性變數,我們建議使用volatile 修飾符,它告訴編譯器無需對該變數作任何的優化,即無需將它放到一個暫存器中,並且該值可被外部改變。如果執行緒間所需傳遞的資訊較複雜,我們可以定義一個結構,通過傳遞指向該結構的指標進行傳遞資訊。
- 使用自定義訊息
我們可以在一個執行緒的執行函式中向另一個執行緒傳送自定義的訊息來達到通訊的目的。一個執行緒向另外一個執行緒傳送訊息是通過作業系統實現的。利用Windows作業系統的訊息驅動機制,當一個執行緒發出一條訊息時,作業系統首先接收到該訊息,然後把該訊息轉發給目標執行緒,接收訊息的執行緒必須已經建立了訊息迴圈。
例程7 MultiThread7
該例程演示瞭如何使用自定義訊息進行執行緒間通訊。首先,主執行緒向CCalculateThread執行緒傳送訊息WM_CALCULATE,CCalculateThread執行緒收到訊息後進行計算,再向主執行緒傳送WM_DISPLAY訊息,主執行緒收到該訊息後顯示計算結果。
- 建立一個基於對話方塊的工程MultiThread7,在對話方塊IDD_MULTITHREAD7_DIALOG中加入三個單選按鈕IDC_RADIO1,IDC_RADIO2,IDC_RADIO3,標題分別為1+2+3+4+......+10,1+2+3+4+......+50,1+2+3+4+......+100。加入按鈕IDC_SUM,標題為“求和”。加入標籤框IDC_STATUS,屬性選中“邊框”;
- 在MultiThread7Dlg.h中定義如下變數:
protected: int nAddend;
代表加數的大小。
分別雙擊三個單選按鈕,新增訊息響應函式:void CMultiThread7Dlg::OnRadio1() { nAddend=10; } void CMultiThread7Dlg::OnRadio2() { nAddend=50; } void CMultiThread7Dlg::OnRadio3() { nAddend=100; }
- BOOL CMultiThread7Dlg::OnInitDialog()
- {
- ……
- ((CButton*)GetDlgItem(IDC_RADIO1))->SetCheck(TRUE);
- nAddend=10;
- ……
- #include "CalculateThread.h"
- #define WM_DISPLAY WM_USER+2
- class CMultiThread7Dlg : public CDialog
- {
- // Construction
- public:
- CMultiThread7Dlg(CWnd* pParent = NULL); // standard constructor
- CCalculateThread* m_pCalculateThread;
- ……
- protected:
- int nAddend;
- LRESULT OnDisplay(WPARAM wParam,LPARAM lParam);
- ……
- 在MultiThread7Dlg.cpp中新增:
- BEGIN_MESSAGE_MAP(CMultiThread7Dlg, CDialog)
- ……
- ON_MESSAGE(WM_DISPLAY,OnDisplay)
- END_MESSAGE_MAP()
- LRESULT CMultiThread7Dlg::OnDisplay(WPARAM wParam,LPARAM lParam)
- {
- int nTemp=(int)wParam;
- SetDlgItemInt(IDC_STATUS,nTemp,FALSE);
- return 0;
- }
- void CMultiThread7Dlg::OnSum()
- {
- m_pCalculateThread=
- (CCalculateThread*)AfxBeginThread(RUNTIME_CLASS(CCalculateThread));
- Sleep(500);
- m_pCalculateThread->PostThreadMessage(WM_CALCULATE,nAddend,NULL);
- }
在檔案CalculateThread.h 中新增
- #define WM_CALCULATE WM_USER+1
- class CCalculateThread : public CWinThread
- {
- ……
- protected:
- afx_msg LONG OnCalculate(UINT wParam,LONG lParam);
- ……
- 在檔案CalculateThread.cpp中新增
- LONG CCalculateThread::OnCalculate(UINT wParam,LONG lParam)
- {
- int nTmpt=0;
- for(int i=0;i<=(int)wParam;i++)
- {
- nTmpt=nTmpt+i;
- }
- Sleep(500);
- ::PostMessage((HWND)(GetMainWnd()->GetSafeHwnd()),WM_DISPLAY,nTmpt,NULL);
- return 0;
- }
- BEGIN_MESSAGE_MAP(CCalculateThread, CWinThread)
- //{{AFX_MSG_MAP(CCalculateThread)
- // NOTE - the ClassWizard will add and remove mapping macros here.
- //}}AFX_MSG_MAP
- ON_THREAD_MESSAGE(WM_CALCULATE,OnCalculate)
- //和主執行緒對比,注意它們的區別
- END_MESSAGE_MAP()
#include "MultiThread7Dlg.h"以上程式碼為 CCalculateThread 類添加了 WM_CALCULATE 訊息,訊息的響應函式是 OnCalculate,其功能是根據引數 wParam 的值,進行累加,累加結果在臨時變數nTmpt中,延時0.5秒,向主執行緒傳送WM_DISPLAY訊息進行顯示,nTmpt作為引數傳遞。
編譯並執行該例程,體會如何線上程間傳遞訊息。
執行緒的同步
雖然多執行緒能給我們帶來好處,但是也有不少問題需要解決。例如,對於像磁碟驅動器這樣獨佔性系統資源,由於執行緒可以執行程序的任何程式碼段,且執行緒的執行是由系統排程自動完成的,具有一定的不確定性,因此就有可能出現兩個執行緒同時對磁碟驅動器進行操作,從而出現操作錯誤;又例如,對於銀行系統的計算機來說,可能使用一個執行緒來更新其使用者資料庫,而用另外一個執行緒來讀取資料庫以響應儲戶的需要,極有可能讀資料庫的執行緒讀取的是未完全更新的資料庫,因為可能在讀的時候只有一部分資料被更新過。
使隸屬於同一程序的各執行緒協調一致地工作稱為執行緒的同步。MFC提供了多種同步物件,下面我們只介紹最常用的四種:
- 臨界區(CCriticalSection)
- 事件(CEvent)
- 互斥量(CMutex)
- 訊號量(CSemaphore)
通過這些類,我們可以比較容易地做到執行緒同步。
A、使用 CCriticalSection 類
當多個執行緒訪問一個獨佔性共享資源時,可以使用“臨界區”物件。任一時刻只有一個執行緒可以擁有臨界區物件,擁有臨界區的執行緒可以訪問被保護起來的資源或程式碼段,其他希望進入臨界區的執行緒將被掛起等待,直到擁有臨界區的執行緒放棄臨界區時為止,這樣就保證了不會在同一時刻出現多個執行緒訪問共享資源。
CCriticalSection類的用法非常簡單,步驟如下:
- 定義CCriticalSection類的一個全域性物件(以使各個執行緒均能訪問),如CCriticalSection critical_section;
- 在訪問需要保護的資源或程式碼之前,呼叫CCriticalSection類的成員Lock()獲得臨界區物件:
critical_section.Lock();
線上程中呼叫該函式來使執行緒獲得它所請求的臨界區。如果此時沒有其它執行緒佔有臨界區物件,則呼叫Lock()的執行緒獲得臨界區;否則,執行緒將被掛起,並放入到一個系統佇列中等待,直到當前擁有臨界區的執行緒釋放了臨界區時為止。 - 訪問臨界區完畢後,使用CCriticalSection的成員函式Unlock()來釋放臨界區:
critical_section.Unlock();
再通俗一點講,就是執行緒A執行到critical_section.Lock();語句時,如果其它執行緒(B)正在執行critical_section.Lock();語句後且critical_section. Unlock();語句前的語句時,執行緒A就會等待,直到執行緒B執行完critical_section. Unlock();語句,執行緒A才會繼續執行。
下面再通過一個例項進行演示說明。
例程8 MultiThread8
- 建立一個基於對話方塊的工程MultiThread8,在對話方塊IDD_MULTITHREAD8_DIALOG中加入兩個按鈕和兩個編輯框控制元件,兩個按鈕的ID分別為IDC_WRITEW和IDC_WRITED,標題分別為“寫‘W’”和“寫‘D’”;兩個編輯框的ID分別為IDC_W和IDC_D,屬性都選中Read-only;
- 在MultiThread8Dlg.h檔案中宣告兩個執行緒函式:
UINT WriteW(LPVOID pParam); UINT WriteD(LPVOID pParam);
- 使用ClassWizard分別給IDC_W和IDC_D新增CEdit類變數m_ctrlW和m_ctrlD;
- 在MultiThread8Dlg.cpp檔案中新增如下內容:
為了檔案中能夠正確使用同步類,在檔案開頭新增: #include "afxmt.h" 定義臨界區和一個字元陣列,為了能夠在不同執行緒間使用,定義為全域性變數: CCriticalSection critical_section; char g_Array[10]; 新增執行緒函式: UINT WriteW(LPVOID pParam) { CEdit *pEdit=(CEdit*)pParam; pEdit->SetWindowText(""); critical_section.Lock(); //鎖定臨界區,其它執行緒遇到critical_section.Lock();語句時要等待 //直至執行critical_section.Unlock();語句 for(int i=0;i<10;i++) { g_Array[i]=''W''; pEdit->SetWindowText(g_Array); Sleep(1000); } critical_section.Unlock(); return 0; } UINT WriteD(LPVOID pParam) { CEdit *pEdit=(CEdit*)pParam; pEdit->SetWindowText(""); critical_section.Lock(); //鎖定臨界區,其它執行緒遇到critical_section.Lock();語句時要等待 //直至執行critical_section.Unlock();語句 for(int i=0;i<10;i++) { g_Array[i]=''D''; pEdit->SetWindowText(g_Array); Sleep(1000); } critical_section.Unlock(); return 0; } 分別雙擊按鈕IDC_WRITEW和IDC_WRITED,新增其響應函式: void CMultiThread8Dlg::OnWritew() { CWinThread *pWriteW=AfxBeginThread(WriteW, &m_ctrlW, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pWriteW->ResumeThread(); } void CMultiThread8Dlg::OnWrited() { CWinThread *pWriteD=AfxBeginThread(WriteD, &m_ctrlD, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pWriteD->ResumeThread(); }
-
由於程式碼較簡單,不再詳述。編譯、執行該例程,您可以連續點選兩個按鈕,觀察體會臨界類的作用。
B、使用 CEvent 類
CEvent 類提供了對事件的支援。事件是一個允許一個執行緒在某種情況發生時,喚醒另外一個執行緒的同步物件。例如在某些網路應用程式中,一個執行緒(記為A)負責監聽通訊埠,另外一個執行緒(記為B)負責更新使用者資料。通過使用CEvent 類,執行緒A可以通知執行緒B何時更新使用者資料。每一個CEvent 物件可以有兩種狀態:有訊號狀態和無訊號狀態。執行緒監視位於其中的CEvent 類物件的狀態,並在相應的時候採取相應的操作。
在MFC中,CEvent 類物件有兩種型別:人工事件和自動事件。一個自動CEvent 物件在被至少一個執行緒釋放後會自動返回到無訊號狀態;而人工事件物件獲得訊號後,釋放可利用執行緒,但直到呼叫成員函式ReSetEvent()才將其設定為無訊號狀態。在建立CEvent 類的物件時,預設建立的是自動事件。 CEvent 類的各成員函式的原型和引數說明如下:
1、CEvent(BOOL bInitiallyOwn=FALSE, BOOL bManualReset=FALSE, LPCTSTR lpszName=NULL, LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);
- bInitiallyOwn:指定事件物件初始化狀態,TRUE為有訊號,FALSE為無訊號;
- bManualReset:指定要建立的事件是屬於人工事件還是自動事件。TRUE為人工事件,FALSE為自動事件;
- 後兩個引數一般設為NULL,在此不作過多說明。
2、BOOL CEvent::SetEvent();
將 CEvent 類物件的狀態設定為有訊號狀態。如果事件是人工事件,則 CEvent 類物件保持為有訊號狀態,直到呼叫成員函式ResetEvent()將 其重新設為無訊號狀態時為止。如果CEvent 類物件為自動事件,則在SetEvent()將事件設定為有訊號狀態後,CEvent 類物件由系統自動重置為無訊號狀態。
如果該函式執行成功,則返回非零值,否則返回零。
3、BOOL CEvent::ResetEvent();
該函式將事件的狀態設定為無訊號狀態,並保持該狀態直至SetEvent()被呼叫時為止。由於自動事件是由系統自動重置,故自動事件不需要呼叫該函式。如果該函式執行成功,返回非零值,否則返回零。我們一般通過呼叫WaitForSingleObject函式來監視事件狀態。前面我們已經介紹了該函式。由於語言描述的原因,CEvent 類的理解確實有些難度,但您只要通過仔細玩味下面例程,多看幾遍就可理解。
例程9 MultiThread9
- 建立一個基於對話方塊的工程MultiThread9,在對話方塊IDD_MULTITHREAD9_DIALOG中加入一個按鈕和兩個編輯框控制元件,按鈕的ID為IDC_WRITEW,標題為“寫‘W’”;兩個編輯框的ID分別為IDC_W和IDC_D,屬性都選中Read-only;
- 在MultiThread9Dlg.h檔案中宣告兩個執行緒函式:
UINT WriteW(LPVOID pParam); UINT WriteD(LPVOID pParam);
- 使用ClassWizard分別給IDC_W和IDC_D新增CEdit類變數m_ctrlW和m_ctrlD;
- 在MultiThread9Dlg.cpp檔案中新增如下內容:
為了檔案中能夠正確使用同步類,在檔案開頭新增
#include "afxmt.h" 定義事件物件和一個字元陣列,為了能夠在不同執行緒間使用,定義為全域性變數。 CEvent eventWriteD; char g_Array[10]; 新增執行緒函式: UINT WriteW(LPVOID pParam) { CEdit *pEdit=(CEdit*)pParam; pEdit->SetWindowText(""); for(int i=0;i<10;i++) { g_Array[i]=''W''; pEdit->SetWindowText(g_Array); Sleep(1000); } eventWriteD.SetEvent(); return 0; } UINT WriteD(LPVOID pParam) { CEdit *pEdit=(CEdit*)pParam; pEdit->SetWindowText(""); WaitForSingleObject(eventWriteD.m_hObject,INFINITE); for(int i=0;i<10;i++) { g_Array[i]=''D''; pEdit->SetWindowText(g_Array); Sleep(1000); } return 0; }
仔細分析這兩個執行緒函式, 您就會正確理解CEvent 類。執行緒WriteD執行到 WaitForSingleObject(eventWriteD.m_hObject,INFINITE);處等待,直到事件eventWriteD為有訊號該執行緒才往下執行,因為eventWriteD物件是自動事件,則當WaitForSingleObject()返回時,系統自動把eventWriteD物件重置為無訊號狀態。 - 雙擊按鈕IDC_WRITEW,新增其響應函式:
void CMultiThread9Dlg::OnWritew() { CWinThread *pWriteW=AfxBeginThread(WriteW, &m_ctrlW, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pWriteW->ResumeThread(); CWinThread *pWriteD=AfxBeginThread(WriteD, &m_ctrlD, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pWriteD->ResumeThread(); }
編譯並執行程式,單擊“寫‘W’”按鈕,體會事件物件的作用。
C、使用CMutex 類
互斥物件與臨界區物件很像.互斥物件與臨界區物件的不同在於:互斥物件可以在程序間使用,而臨界區物件只能在同一程序的各執行緒間使用。當然,互斥物件也可以用於同一程序的各個執行緒間,但是在這種情況下,使用臨界區會更節省系統資源,更有效率。
D、使用CSemaphore 類
當需要一個計數器來限制可以使用某個執行緒的數目時,可以使用“訊號量”物件。CSemaphore 類的物件儲存了對當前訪問某一指定資源的執行緒的計數值,該計數值是當前還可以使用該資源的執行緒的數目。如果這個計數達到了零,則所有對這個CSemaphore 類物件所控制的資源的訪問嘗試都被放入到一個佇列中等待,直到超時或計數值不為零時為止。一個執行緒被釋放已訪問了被保護的資源時,計數值減1;一個執行緒完成了對被控共享資源的訪問時,計數值增1。這個被CSemaphore 類物件所控制的資源可以同時接受訪問的最大執行緒數在該物件的構建函式中指定。
CSemaphore 類的建構函式原型及引數說明如下:
CSemaphore (LONG lInitialCount=1, LONG lMaxCount=1, LPCTSTR pstrName=NULL, LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);
- lInitialCount:訊號量物件的初始計數值,即可訪問執行緒數目的初始值;
- lMaxCount:訊號量物件計數值的最大值,該引數決定了同一時刻可訪問由訊號量保護的資源的執行緒最大數目;
- 後兩個引數在同一程序中使用一般為NULL,不作過多討論;
在用CSemaphore 類的建構函式建立訊號量物件時要同時指出允許的最大資源計數和當前可用資源計數。一般是將當前可用資源計數設定為最大資源計數,每增加一個執行緒對共享資源的訪問,當前可用資源計數就會減1,只要當前可用資源計數是大於0的,就可以發出訊號量訊號。但是當前可用計數減小到0時,則說明當前佔用資源的執行緒數已經達到了所允許的最大數目,不能再允許其它執行緒的進入,此時的訊號量訊號將無法發出。執行緒在處理完共享資源後,應在離開的同時通過ReleaseSemaphore()函式將當前可用資源數加1。
下面給出一個簡單例項來說明 CSemaphore 類的用法。
例程10 MultiThread10
- 建立一個基於對話方塊的工程MultiThread10,在對話方塊IDD_MULTITHREAD10_DIALOG中加入一個按鈕和三個編輯框控制元件,按鈕的ID為IDC_START,標題為“同時寫‘A’、‘B’、‘C’”;三個編輯框的ID分別為IDC_A、IDC_B和IDC_C,屬性都選中Read-only;
- 在MultiThread10Dlg.h檔案中宣告兩個執行緒函式:
UINT WriteA(LPVOID pParam); UINT WriteB(LPVOID pParam); UINT WriteC(LPVOID pParam);
- 使用ClassWizard分別給IDC_A、IDC_B和IDC_C新增CEdit類變數m_ctrlA、m_ctrlB和m_ctrlC;
- 在MultiThread10Dlg.cpp檔案中新增如下內容:
為了檔案中能夠正確使用同步類,在檔案開頭新增:
#include "afxmt.h" 定義訊號量物件和一個字元陣列,為了能夠在不同執行緒間使用,定義為全域性變數: CSemaphore semaphoreWrite(2,2); //資源最多訪問執行緒2個,當前可訪問執行緒數2個 char g_Array[10]; 新增三個執行緒函式:
UINT WriteA(LPVOID pParam) { CEdit *pEdit=(CEdit*)pParam; pEdit->SetWindowText(""); WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE); CString str; for(int i=0;i<10;i++) { pEdit->GetWindowText(str); g_Array[i]=''A''; str=str+g_Array[i]; pEdit->SetWindowText(str); Sleep(1000); } ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL); return 0; } UINT WriteB(LPVOID pParam) { CEdit *pEdit=(CEdit*)pParam; pEdit->SetWindowText(""); WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE); CString str; for(int i=0;i<10;i++) { pEdit->GetWindowText(str); g_Array[i]=''B''; str=str+g_Array[i]; pEdit->SetWindowText(str); Sleep(1000); } ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL); return 0; } UINT WriteC(LPVOID pParam) { CEdit *pEdit=(CEdit*)pParam; pEdit->SetWindowText(""); WaitForSingleObject(semaphoreWrite.m_hObject,INFINITE); for(int i=0;i<10;i++) { g_Array[i]=''C''; pEdit->SetWindowText(g_Array); Sleep(1000); } ReleaseSemaphore(semaphoreWrite.m_hObject,1,NULL); return 0; }
這三個執行緒函式不再多說。在訊號量物件有訊號的狀態下,執行緒執行到WaitForSingleObject語句處繼續執行,同時可用執行緒數減1;若執行緒執行到WaitForSingleObject語句時訊號量物件無訊號,執行緒就在這裡等待,直到訊號量物件有訊號執行緒才往下執行。 - 雙擊按鈕IDC_START,新增其響應函式:
void CMultiThread10Dlg::OnStart() { CWinThread *pWriteA=AfxBeginThread(WriteA, &m_ctrlA, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pWriteA->ResumeThread(); CWinThread *pWriteB=AfxBeginThread(WriteB, &m_ctrlB, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pWriteB->ResumeThread(); CWinThread *pWriteC=AfxBeginThread(WriteC, &m_ctrlC, THREAD_PRIORITY_NORMAL, 0, CREATE_SUSPENDED); pWriteC->ResumeThread(); }