Part of the red marker to add by YeHao,Order oneself to better understand.
八、線程的同步
雖然多線程能給我們帶來好處,但是也有不少問題需要解決。例如,對于像磁盤驅(qū)動器這樣獨占性系統(tǒng)資源,由于線程可以執(zhí)行進(jìn)程的任何代碼段,且線程的運行是由系統(tǒng)調(diào)度自動完成的,具有一定的不確定性,因此就有可能出現(xiàn)兩個線程同時對磁盤驅(qū)動器進(jìn)行操作,從而出現(xiàn)操作錯誤;又例如,對于銀行系統(tǒng)的計算機(jī)來說,可能使用一個線程來更新其用戶數(shù)據(jù)庫,而用另外一個線程來讀取數(shù)據(jù)庫以響應(yīng)儲戶的需要,極有可能讀數(shù)據(jù)庫的線程讀取的是未完全更新的數(shù)據(jù)庫,因為可能在讀的時候只有一部分?jǐn)?shù)據(jù)被更新過。
使隸屬于同一進(jìn)程的各線程協(xié)調(diào)一致地工作稱為線程的同步。MFC提供了多種同步對象,下面我們只介紹最常用的四種:
- 臨界區(qū)(CCriticalSection)
- 事件(CEvent)
- 互斥量(CMutex)
- 信號量(CSemaphore)
通過這些類,我們可以比較容易地做到線程同步。
A、使用 CCriticalSection 類
當(dāng)多個線程訪問一個獨占性共享資源時,可以使用“臨界區(qū)”對象。任一時刻只有一個線程可以擁有臨界區(qū)對象,擁有臨界區(qū)的線程可以訪問被保護(hù)起來的資源或代碼段,其他希望進(jìn)入臨界區(qū)的線程將被掛起等待,直到擁有臨界區(qū)的線程放棄臨界區(qū)時為止,這樣就保證了不會在同一時刻出現(xiàn)多個線程訪問共享資源。
CCriticalSection類的用法非常簡單,步驟如下:
- 定義CCriticalSection類的一個全局對象(以使各個線程均能訪問),如CCriticalSection critical_section;
- 在訪問需要保護(hù)的資源或代碼之前,調(diào)用CCriticalSection類的成員Lock()獲得臨界區(qū)對象:
critical_section.Lock();
在線程中調(diào)用該函數(shù)來使線程獲得它所請求的臨界區(qū)。如果此時沒有其它線程占有臨界區(qū)對象,則調(diào)用Lock()的線程獲得臨界區(qū);否則,線程將被掛起,并放入到一個系統(tǒng)隊列中等待,直到當(dāng)前擁有臨界區(qū)的線程釋放了臨界區(qū)時為止。
- 訪問臨界區(qū)完畢后,使用CCriticalSection的成員函數(shù)Unlock()來釋放臨界區(qū):
critical_section.Unlock();
再通俗一點講,就是線程A執(zhí)行到critical_section.Lock();語句時,如果其它線程(B)正在執(zhí)行critical_section.Lock();語句后且critical_section. Unlock();語句前的語句時,線程A就會等待,直到線程B執(zhí)行完critical_section. Unlock();語句,線程A才會繼續(xù)執(zhí)行。
下面再通過一個實例進(jìn)行演示說明。
例程8 MultiThread8
- 建立一個基于對話框的工程MultiThread8,在對話框IDD_MULTITHREAD8_DIALOG中加入兩個按鈕和兩個編輯框控件,兩個按鈕的ID分別為IDC_WRITEW和IDC_WRITED,標(biāo)題分別為“寫‘W’”和“寫‘D’”;兩個編輯框的ID分別為IDC_W和IDC_D,屬性都選中Read-only;
- 在MultiThread8Dlg.h文件中聲明兩個線程函數(shù):
UINT WriteW(LPVOID pParam);
UINT WriteD(LPVOID pParam);
- 使用ClassWizard分別給IDC_W和IDC_D添加CEdit類變量m_ctrlW和m_ctrlD;
- 在MultiThread8Dlg.cpp文件中添加如下內(nèi)容:
為了文件中能夠正確使用同步類,在文件開頭添加:
#include "afxmt.h"
定義臨界區(qū)和一個字符數(shù)組,為了能夠在不同線程間使用,定義為全局變量:
CCriticalSection critical_section;
char g_Array[10];
添加線程函數(shù):
UINT WriteW(LPVOID pParam)
{
CEdit *pEdit=(CEdit*)pParam;
pEdit->SetWindowText("");
critical_section.Lock();
//鎖定臨界區(qū),其它線程遇到critical_section.Lock();語句時要等待
//直至執(zhí)行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();
//鎖定臨界區(qū),其它線程遇到critical_section.Lock();語句時要等待
//直至執(zhí)行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,添加其響應(yīng)函數(shù):
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();
}
由于代碼較簡單,不再詳述。編譯、運行該例程,您可以連續(xù)點擊兩個按鈕,觀察體會臨界類的作用。
B、使用 CEvent 類
CEvent 類提供了對事件的支持。事件是一個允許一個線程在某種情況發(fā)生時,喚醒另外一個線程的同步對象。例如在某些網(wǎng)絡(luò)應(yīng)用程序中,一個線程(記為A)負(fù)責(zé)監(jiān)聽通訊端口,另外一個線程(記為B)負(fù)責(zé)更新用戶數(shù)據(jù)。通過使用CEvent 類,線程A可以通知線程B何時更新用戶數(shù)據(jù)。每一個CEvent 對象可以有兩種狀態(tài):有信號狀態(tài)和無信號狀態(tài)。線程監(jiān)視位于其中的CEvent 類對象的狀態(tài),并在相應(yīng)的時候采取相應(yīng)的操作。
在MFC中,CEvent 類對象有兩種類型:人工事件和自動事件。一個自動CEvent 對象在被至少一個線程釋放后會自動返回到無信號狀態(tài);而人工事件對象獲得信號后,釋放可利用線程,但直到調(diào)用成員函數(shù)ReSetEvent()才將其設(shè)置為無信號狀態(tài)。在創(chuàng)建CEvent 類的對象時,默認(rèn)創(chuàng)建的是自動事件。 CEvent 類的各成員函數(shù)的原型和參數(shù)說明如下:
1、CEvent(BOOL bInitiallyOwn=FALSE,
BOOL bManualReset=FALSE,
LPCTSTR lpszName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttribute=NULL);
- bInitiallyOwn:指定事件對象初始化狀態(tài),TRUE為有信號,F(xiàn)ALSE為無信號;
- bManualReset:指定要創(chuàng)建的事件是屬于人工事件還是自動事件。TRUE為人工事件,F(xiàn)ALSE為自動事件;
- 后兩個參數(shù)一般設(shè)為NULL,在此不作過多說明。
2、BOOL CEvent::SetEvent();
將 CEvent 類對象的狀態(tài)設(shè)置為有信號狀態(tài)。如果事件是人工事件,則 CEvent 類對象保持為有信號狀態(tài),直到調(diào)用成員函數(shù)ResetEvent()將 其重新設(shè)為無信號狀態(tài)時為止。如果CEvent 類對象為自動事件,則在SetEvent()將事件設(shè)置為有信號狀態(tài)后,CEvent 類對象由系統(tǒng)自動重置為無信號狀態(tài)。
如果該函數(shù)執(zhí)行成功,則返回非零值,否則返回零。
3、BOOL CEvent::ResetEvent();
該函數(shù)將事件的狀態(tài)設(shè)置為無信號狀態(tài),并保持該狀態(tài)直至SetEvent()被調(diào)用時為止。由于自動事件是由系統(tǒng)自動重置,故自動事件不需要調(diào)用該函數(shù)。如果該函數(shù)執(zhí)行成功,返回非零值,否則返回零。我們一般通過調(diào)用WaitForSingleObject函數(shù)來監(jiān)視事件狀態(tài)。前面我們已經(jīng)介紹了該函數(shù)。由于語言描述的原因,CEvent 類的理解確實有些難度,但您只要通過仔細(xì)玩味下面例程,多看幾遍就可理解。
例程9 MultiThread9
- 建立一個基于對話框的工程MultiThread9,在對話框IDD_MULTITHREAD9_DIALOG中加入一個按鈕和兩個編輯框控件,按鈕的ID為IDC_WRITEW,標(biāo)題為“寫‘W’”;兩個編輯框的ID分別為IDC_W和IDC_D,屬性都選中Read-only;
- 在MultiThread9Dlg.h文件中聲明兩個線程函數(shù):
UINT WriteW(LPVOID pParam);
UINT WriteD(LPVOID pParam);
- 使用ClassWizard分別給IDC_W和IDC_D添加CEdit類變量m_ctrlW和m_ctrlD;
- 在MultiThread9Dlg.cpp文件中添加如下內(nèi)容:
為了文件中能夠正確使用同步類,在文件開頭添加
#include "afxmt.h"
定義事件對象和一個字符數(shù)組,為了能夠在不同線程間使用,定義為全局變量。
//CEvent eventWriteD;//默認(rèn)為無信號狀態(tài)
CEvent eventWriteD(TRUE);
char g_Array[10];
添加線程函數(shù):
UINT WriteW(LPVOID pParam)
{
CEdit *pEdit=(CEdit*)pParam;
pEdit->SetWindowText("");
WaitForSingleObject(eventWriteD.m_hObject,INFINITE);//函數(shù)返回時事件自動重置為無信號狀態(tài)
for(int i=0;i<10;i++)
{
g_Array[i]=''W'';
pEdit->SetWindowText(g_Array);
Sleep(1000);
}
eventWriteD.SetEvent();//將事件置為有信號狀態(tài),使其他線程可以獲得有信號狀態(tài)
return 0;
}
UINT WriteD(LPVOID pParam)
{
CEdit *pEdit=(CEdit*)pParam;
pEdit->SetWindowText("");
WaitForSingleObject(eventWriteD.m_hObject,INFINITE);//當(dāng)事件為無信號狀態(tài)時WaitForSingleObject會阻塞線程,走到事件有信號
for(int i=0;i<10;i++)
{
g_Array[i]=''D'';
pEdit->SetWindowText(g_Array);
Sleep(1000);
}
eventWriteD.SetEvent();
return 0;
}
- 仔細(xì)分析這兩個線程函數(shù), 您就會正確理解CEvent 類。線程WriteD執(zhí)行到 WaitForSingleObject(eventWriteD.m_hObject,INFINITE);處等待,直到事件eventWriteD為有信號該線程才往下執(zhí)行,因為eventWriteD對象是自動事件,則當(dāng)WaitForSingleObject()返回時,系統(tǒng)自動把eventWriteD對象重置為無信號狀態(tài)。
- 雙擊按鈕IDC_WRITEW,添加其響應(yīng)函數(shù):
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 類
互斥對象與臨界區(qū)對象很像.互斥對象與臨界區(qū)對象的不同在于:互斥對象可以在進(jìn)程間使用,而臨界區(qū)對象只能在同一進(jìn)程的各線程間使用。當(dāng)然,互斥對象也可以用于同一進(jìn)程的各個線程間,但是在這種情況下,使用臨界區(qū)會更節(jié)省系統(tǒng)資源,更有效率。
D、使用CSemaphore 類
當(dāng)需要一個計數(shù)器來限制可以使用某個線程的數(shù)目時,可以使用“信號量”對象。CSemaphore 類的對象保存了對當(dāng)前訪問某一指定資源的線程的計數(shù)值,該計數(shù)值是當(dāng)前還可以使用該資源的線程的數(shù)目。如果這個計數(shù)達(dá)到了零,則所有對這個CSemaphore 類對象所控制的資源的訪問嘗試都被放入到一個隊列中等待,直到超時或計數(shù)值不為零時為止。一個線程被釋放已訪問了被保護(hù)的資源時,計數(shù)值減1;一個線程完成了對被控共享資源的訪問時,計數(shù)值增1。這個被CSemaphore 類對象所控制的資源可以同時接受訪問的最大線程數(shù)在該對象的構(gòu)建函數(shù)中指定。
CSemaphore 類的構(gòu)造函數(shù)原型及參數(shù)說明如下:
CSemaphore (LONG lInitialCount=1,
LONG lMaxCount=1,
LPCTSTR pstrName=NULL,
LPSECURITY_ATTRIBUTES lpsaAttributes=NULL);
- lInitialCount:信號量對象的初始計數(shù)值,即可訪問線程數(shù)目的初始值;
- lMaxCount:信號量對象計數(shù)值的最大值,該參數(shù)決定了同一時刻可訪問由信號量保護(hù)的資源的線程最大數(shù)目;
- 后兩個參數(shù)在同一進(jìn)程中使用一般為NULL,不作過多討論;
在用CSemaphore 類的構(gòu)造函數(shù)創(chuàng)建信號量對象時要同時指出允許的最大資源計數(shù)和當(dāng)前可用資源計數(shù)。一般是將當(dāng)前可用資源計數(shù)設(shè)置為最大資源計數(shù),每增加一個線程對共享資源的訪問,當(dāng)前可用資源計數(shù)就會減1,只要當(dāng)前可用資源計數(shù)是大于0的,就可以發(fā)出信號量信號。但是當(dāng)前可用計數(shù)減小到0時,則說明當(dāng)前占用資源的線程數(shù)已經(jīng)達(dá)到了所允許的最大數(shù)目,不能再允許其它線程的進(jìn)入,此時的信號量信號將無法發(fā)出。線程在處理完共享資源后,應(yīng)在離開的同時通過ReleaseSemaphore()函數(shù)將當(dāng)前可用資源數(shù)加1。
下面給出一個簡單實例來說明 CSemaphore 類的用法。
例程10 MultiThread10
- 建立一個基于對話框的工程MultiThread10,在對話框IDD_MULTITHREAD10_DIALOG中加入一個按鈕和三個編輯框控件,按鈕的ID為IDC_START,標(biāo)題為“同時寫‘A’、‘B’、‘C’”;三個編輯框的ID分別為IDC_A、IDC_B和IDC_C,屬性都選中Read-only;
- 在MultiThread10Dlg.h文件中聲明兩個線程函數(shù):
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文件中添加如下內(nèi)容:
為了文件中能夠正確使用同步類,在文件開頭添加:
#include "afxmt.h"
定義信號量對象和一個字符數(shù)組,為了能夠在不同線程間使用,定義為全局變量:
CSemaphore semaphoreWrite(2,2); //資源最多訪問線程2個,當(dāng)前可訪問線程數(shù)2個,應(yīng)該改為(1,1)才能實現(xiàn)同步的效果
char g_Array[10];
添加三個線程函數(shù):
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;
}
這三個線程函數(shù)不再多說。在信號量對象有信號的狀態(tài)下,線程執(zhí)行到WaitForSingleObject語句處繼續(xù)執(zhí)行,同時可用線程數(shù)減1;若線程執(zhí)行到WaitForSingleObject語句時信號量對象無信號,線程就在這里等待,直到信號量對象有信號線程才往下執(zhí)行。
- 雙擊按鈕IDC_START,添加其響應(yīng)函數(shù):
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();
}
好吧,多線程編程就介紹到這里,希望本文能對您有所幫助。