VC++多線程編程-線程間的通訊和線程同步

來源:互聯網
上載者:User

線程間通訊

  一般而言,應用程式中的一個次要線程總是為主線程執行特定的任務,這樣,主線程和次要線程間必定有一個資訊傳遞的渠道,也就是主線程和次要線程間要進行通訊。這種線程間的通訊不但是難以避免的,而且在多線程編程中也是複雜和頻繁的,下面將進行說明。

  1. 使用全域變數進行通訊

    由於屬於同一個進程的各個線程共用作業系統分配該進程的資源,故解決線程間通訊最簡單的一種方法是使用全域變數。對於標準類型的全域變數,我們建議使用volatile 修飾符,它告訴編譯器無需對該變數作任何的最佳化,即無需將它放到一個寄存器中,並且該值可被外部改變。如果線程間所需傳遞的資訊較複雜,我們可以定義一個結構,通過傳遞指向該結構的指標進行傳遞資訊。
     

  2. 使用自訂訊息

    我們可以在一個線程的執行函數中向另一個線程發送自訂的訊息來達到通訊的目的。一個線程向另外一個線程發送訊息是通過作業系統實現的。利用Windows作業系統的訊息驅動機制,當一個線程發出一條訊息時,作業系統首先接收到該訊息,然後把該訊息轉寄給目標線程,接收訊息的線程必須已經建立了訊息迴圈。

常式7 MultiThread7

  該常式示範了如何使用自訂訊息進行線程間通訊。首先,主線程向CCalculateThread線程發送訊息WM_CALCULATE,CCalculateThread線程收到訊息後進行計算,再向主線程發送WM_DISPLAY訊息,主線程收到該訊息後顯示計算結果。

  1. 建立一個基於對話方塊的工程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,屬性選中“邊框”;
  2. 在MultiThread7Dlg.h中定義如下變數:
    protected: int nAddend;

    代表加數的大小。

    分別雙擊三個選項按鈕,添加訊息響應函數:

    void CMultiThread7Dlg::OnRadio1() { nAddend=10;}void CMultiThread7Dlg::OnRadio2() { nAddend=50; }void CMultiThread7Dlg::OnRadio3() { nAddend=100; }

    並在OnInitDialog函數中完成相應的初始化工作:

    BOOL CMultiThread7Dlg::OnInitDialog(){…… ((CButton*)GetDlgItem(IDC_RADIO1))->SetCheck(TRUE); nAddend=10;……

    在MultiThread7Dlg.h中添加:

    #include "CalculateThread.h"#define WM_DISPLAY WM_USER+2class CMultiThread7Dlg : public CDialog{// Constructionpublic: 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;}

    以上代碼使得主線程類CMultiThread7Dlg可以處理WM_DISPLAY訊息,即在IDC_STATUS標籤框中顯示計算結果。

  3. 雙擊按鈕IDC_SUM,添加訊息響應函數:
    void CMultiThread7Dlg::OnSum() { m_pCalculateThread=  (CCalculateThread*)AfxBeginThread(RUNTIME_CLASS(CCalculateThread)); Sleep(500); m_pCalculateThread->PostThreadMessage(WM_CALCULATE,nAddend,NULL);}

    OnSum()函數的作用是建立CalculateThread線程,延時給該線程發送WM_CALCULATE訊息。

  4. 右擊工程並選中“New Class…”為工程添加基類為 CWinThread 派生線程類 CCalculateThread。

    在檔案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()

    在CalculateThread.cpp檔案的開頭添加一條:

    #include "MultiThread7Dlg.h"

      以上代碼為 CCalculateThread 類添加了 WM_CALCULATE 訊息,訊息的響應函數是 OnCalculate,其功能是根據參數 wParam 的值,進行累加,累加結果在臨時變數nTmpt中,延時0.5秒,向主線程發送WM_DISPLAY訊息進行顯示,nTmpt作為參數傳遞。

編譯並運行該常式,體會如何線上程間傳遞訊息。

線程的同步

  雖然多線程能給我們帶來好處,但是也有不少問題需要解決。例如,對於像磁碟機這樣獨佔性系統資源,由於線程可以執行進程的任何程式碼片段,且線程的運行是由系統調度自動完成的,具有一定的不確定性,因此就有可能出現兩個線程同時對磁碟機進行操作,從而出現操作錯誤;又例如,對於銀行系統的電腦來說,可能使用一個線程來更新其使用者資料庫,而用另外一個線程來讀取資料庫以響應儲戶的需要,極有可能讀資料庫的線程讀取的是未完全更新的資料庫,因為可能在讀的時候只有一部分資料被更新過。

  使隸屬於同一進程的各線程協調一致地工作稱為線程的同步。MFC提供了多種同步對象,下面我們只介紹最常用的四種:

  • 臨界區(CCriticalSection)
  • 事件(CEvent)
  • 互斥量(CMutex)
  • 訊號量(CSemaphore)
     

通過這些類,我們可以比較容易地做到線程同步。

A、使用 CCriticalSection 類

  當多個線程訪問一個獨佔性共用資源時,可以使用“臨界區”對象。任一時刻只有一個線程可以擁有臨界區對象,擁有臨界區的線程可以訪問被保護起來的資源或程式碼片段,其他希望進入臨界區的線程將被掛起等待,直到擁有臨界區的線程放棄臨界區時為止,這樣就保證了不會在同一時刻出現多個線程訪問共用資源。

CCriticalSection類的用法非常簡單,步驟如下:
 

  1. 定義CCriticalSection類的一個全域對象(以使各個線程均能訪問),如CCriticalSection critical_section;
  2. 在訪問需要保護的資源或代碼之前,調用CCriticalSection類的成員Lock()獲得臨界區對象:
    critical_section.Lock();

    線上程中調用該函數來使線程獲得它所請求的臨界區。如果此時沒有其它線程佔有臨界區對象,則調用Lock()的線程獲得臨界區;否則,線程將被掛起,並放入到一個系統隊列中等待,直到當前擁有臨界區的線程釋放了臨界區時為止。

  3. 訪問臨界區完畢後,使用CCriticalSection的成員函數Unlock()來釋放臨界區:
    critical_section.Unlock();

    再通俗一點講,就是線程A執行到critical_section.Lock();語句時,如果其它線程(B)正在執行critical_section.Lock();語句後且critical_section. Unlock();語句前的語句時,線程A就會等待,直到線程B執行完critical_section. Unlock();語句,線程A才會繼續執行。

下面再通過一個執行個體進行示範說明。

常式8 MultiThread8

  1. 建立一個基於對話方塊的工程MultiThread8,在對話方塊IDD_MULTITHREAD8_DIALOG中加入兩個按鈕和兩個編輯框控制項,兩個按鈕的ID分別為IDC_WRITEW和IDC_WRITED,標題分別為“寫‘W’”和“寫‘D’”;兩個編輯框的ID分別為IDC_W和IDC_D,屬性都選中Read-only;
  2. 在MultiThread8Dlg.h檔案中聲明兩個線程函數:
    UINT WriteW(LPVOID pParam);UINT WriteD(LPVOID pParam);
  3. 使用ClassWizard分別給IDC_W和IDC_D添加CEdit類變數m_ctrlW和m_ctrlD;
  4. 在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;}
  5. 分別雙擊按鈕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

  1. 建立一個基於對話方塊的工程MultiThread9,在對話方塊IDD_MULTITHREAD9_DIALOG中加入一個按鈕和兩個編輯框控制項,按鈕的ID為IDC_WRITEW,標題為“寫‘W’”;兩個編輯框的ID分別為IDC_W和IDC_D,屬性都選中Read-only;
  2. 在MultiThread9Dlg.h檔案中聲明兩個線程函數:
    UINT WriteW(LPVOID pParam);UINT WriteD(LPVOID pParam);
  3. 使用ClassWizard分別給IDC_W和IDC_D添加CEdit類變數m_ctrlW和m_ctrlD;
  4. 在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對象重設為無訊號狀態。

  5. 雙擊按鈕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

  1. 建立一個基於對話方塊的工程MultiThread10,在對話方塊IDD_MULTITHREAD10_DIALOG中加入一個按鈕和三個編輯框控制項,按鈕的ID為IDC_START,標題為“同時寫‘A’、‘B’、‘C’”;三個編輯框的ID分別為IDC_A、IDC_B和IDC_C,屬性都選中Read-only;
  2. 在MultiThread10Dlg.h檔案中聲明兩個線程函數:
    UINT WriteA(LPVOID pParam);UINT WriteB(LPVOID pParam);UINT WriteC(LPVOID pParam);
  3. 使用ClassWizard分別給IDC_A、IDC_B和IDC_C添加CEdit類變數m_ctrlA、m_ctrlB和m_ctrlC;
  4. 在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語句時訊號量對象無訊號,線程就在這裡等待,直到訊號量對象有訊號線程才往下執行。

  5. 雙擊按鈕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(); }

好吧,多線程編程就介紹到這裡,希望本文能對您有所協助。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.