臨界區

因爲進程/線程間的操做是並行進行的,因此就產生了一個數據的問題同步,咱們先看一段代碼: 

int iCounter=0;//全局變量
DOWRD threadA(void* pD)
{
for(int i=0;i<100;i++)
{
int iCopy=iCounter;
//Sleep(1000);
iCopy++;
//Sleep(1000);
iCounter=iCopy;
}
}

如今假設有兩個線程threadA1和threadA2在同時運行那麼運行結束後iCounter的值會是多少,是200嗎?不是的,若是咱們將Sleep(1000)前的註釋去掉後咱們會很容易明白這個問題,由於在iCounter的值被正確修改前它可能已經被其餘的線程修改了。這個例子是一個將機器代碼操做放大的例子,由於在CPU內部也會經歷數據讀/寫的過程,而在線程執行的過程當中線程可能被中斷而讓其餘線程執行。變量iCounter在被第一個線程修改後,寫回內存前若是它又被第二個線程讀取,而後才被第一個線程寫回,那麼第二個線程讀取的實際上是錯誤的數據,這種狀況就稱爲髒讀(dirty read)。這個例子一樣能夠推廣到對文件,資源的使用上。 
那麼要如何才能避免這一問題呢,假設咱們在使用iCounter前向其餘線程詢問一下:有誰在用嗎?若是沒被使用則能夠當即對該變量進行操做,不然等其餘線程使用完後再使用,並且在本身獲得該變量的控制權後其餘線程將不能使用這一變量,直到本身也使用完並釋放爲止。通過修改的僞代碼以下: 

int iCounter=0;//全局變量
DOWRD threadA(void* pD)
{
for(int i=0;i<100;i++)
{
ask to lock iCounter
wait other thread release the lock
lock successful

{
int iCopy=iCounter;
//Sleep(1000);
iCopy++;
}
iCounter=iCopy;

release lock of iCounter
}
}

幸運的是OS提供了多種同步對象供咱們使用,而且能夠替咱們管理同步對象的加鎖和解鎖。咱們須要作的就是對每一個須要同步使用的資源產生一個同步對象,在使用該資源前申請加鎖,在使用完成後解鎖。接下來咱們介紹一些同步對象: 

臨界區:臨界區是一種最簡單的同步對象,它只能夠在同一進程內部使用。它的做用是保證只有一個線程能夠申請到該對象,例如上面的例子咱們就可使用臨界區來進行同步處理。幾個相關的API函數爲: 

VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection );產生臨界區 
VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection );刪除臨界區 
VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );進入臨界區,至關於申請加鎖,若是該臨界區正被其餘線程使用則該函數會等待到其餘線程釋放 
BOOL TryEnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection );進入臨界區,至關於申請加鎖,和EnterCriticalSection不一樣若是該臨界區正被其餘線程使用則該函數會當即返回FALSE,而不會等待 
VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection );退出臨界區,至關於申請解鎖 
下面的示範代碼演示瞭如何使用臨界區來進行數據同步處理: 

//全局變量
int iCounter=0;
CRITICAL_SECTION criCounter;

DWORD threadA(void* pD)
{
int iID=(int)pD;
for(int i=0;i<8;i++)
{
EnterCriticalSection(&criCounter);
int iCopy=iCounter;
Sleep(100);
iCounter=iCopy+1;
printf("thread %d : %d\n",iID,iCounter);
LeaveCriticalSection(&criCounter);
}
return 0;
}
//in main function
{
//建立臨界區
InitializeCriticalSection(&criCounter);
//建立線程
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
//至於WaitForMultipleObjects的用法後面會講到。
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);
//刪除臨界區
DeleteCriticalSection(&criCounter);
printf("\nover\n");

}

接下來要講互斥量與臨界區的做用很是類似,但互斥量是能夠命名的,也就是說它能夠跨越進程使用。因此建立互斥量須要的資源更多,因此若是隻爲了在進程內部是用的話使用臨界區會帶來速度上的優點並可以減小資源佔用量。由於互斥量是跨進程的互斥量一旦被建立,就能夠經過名字打開它。下面介紹能夠用在互斥量上的API函數: 

建立互斥量:
HANDLE CreateMutex(
  LPSECURITY_ATTRIBUTES lpMutexAttributes,// 安全信息
  BOOL bInitialOwner,  // 最初狀態,
  //若是設置爲真,則表示建立它的線程直接擁有了該互斥量,而不須要再申請
  LPCTSTR lpName       // 名字,能夠爲NULL,但這樣一來就不能被其餘線程/進程打開
);
打開一個存在的互斥量:
HANDLE OpenMutex(
  DWORD dwDesiredAccess,  // 存取方式
  BOOL bInheritHandle,    // 是否能夠被繼承
  LPCTSTR lpName          // 名字
);
釋放互斥量的使用權,但要求調用該函數的線程擁有該互斥量的使用權:
BOOL ReleaseMutex(//做用如同LeaveCriticalSection
  HANDLE hMutex   // 句柄
);
關閉互斥量:
BOOL CloseHandle(
  HANDLE hObject   // 句柄
);

你會說爲何沒有名稱如同EnterMutex,功能如同EnterCriticalSection同樣的函數來得到互斥量的使用權呢?的確沒有!獲取互斥量的使用權須要使用函數: 

DWORD WaitForSingleObject(
  HANDLE hHandle,        // 等待的對象的句柄
  DWORD dwMilliseconds   // 等待的時間,以ms爲單位,若是爲INFINITE表示無限期的等待
);
返回:
WAIT_ABANDONED 在等待的對象爲互斥量時代表由於互斥量被關閉而變爲有信號狀態
WAIT_OBJECT_0 獲得使用權
WAIT_TIMEOUT 超過(dwMilliseconds)規定時間

在線程調用WaitForSingleObject後,若是一直沒法獲得控制權線程講被掛起,直到超過期間或是得到控制權。 

講到這裏咱們必須更深刻的講一下WaitForSingleObject函數中的對象(Object)的含義,這裏的對象是一個具備信號狀態的對象,對象有兩種狀態:有信號/無信號。而等待的含義就在於等待對象變爲有信號的狀態,對於互斥量來說若是正在被使用則爲無信號狀態,被釋放後變爲有信號狀態。當等待成功後WaitForSingleObject函數會將互斥量置爲無信號狀態,這樣其餘的線程就不能得到使用權而須要繼續等待。WaitForSingleObject函數還進行排隊功能,保證先提出等待請求的線程先得到對象的使用權,下面的代碼演示瞭如何使用互斥量來進行同步,代碼的功能仍是進行全局變量遞增,經過輸出結果能夠看出,先提出請求的線程先得到了控制權: 

int iCounter=0;

DWORD threadA(void* pD)
{
int iID=(int)pD;
//在內部從新打開
HANDLE hCounterIn=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam sp 44");

for(int i=0;i<8;i++)
{
printf("%d wait for object\n",iID);
WaitForSingleObject(hCounterIn,INFINITE);
int iCopy=iCounter;
Sleep(100);
iCounter=iCopy+1;
printf("\t\tthread %d : %d\n",iID,iCounter);
ReleaseMutex(hCounterIn);
}
CloseHandle(hCounterIn);
return 0;
}

//in main function
{
//建立互斥量
HANDLE hCounter=NULL;
if( (hCounter=OpenMutex(MUTEX_ALL_ACCESS,FALSE,"sam sp 44"))==NULL)
{
//若是沒有其餘進程建立這個互斥量,則從新建立
hCounter = CreateMutex(NULL,FALSE,"sam sp 44");
}

//建立線程
HANDLE hThread[3];
CWinThread* pT1=AfxBeginThread((AFX_THREADPROC)threadA,(void*)1);
CWinThread* pT2=AfxBeginThread((AFX_THREADPROC)threadA,(void*)2);
CWinThread* pT3=AfxBeginThread((AFX_THREADPROC)threadA,(void*)3);
hThread[0]=pT1->m_hThread;
hThread[1]=pT2->m_hThread;
hThread[2]=pT3->m_hThread;
//等待線程結束
WaitForMultipleObjects(3,hThread,TRUE,INFINITE);

//關閉句柄
CloseHandle(hCounter);
}
}

在這裏我沒有使用全局變量來保存互斥量句柄,這並非由於不能這樣作,而是爲演示如何在其餘的代碼段中經過名字來打開已經建立的互斥量。其實這個例子在邏輯上是有一點錯誤的,由於iCounter這個變量沒有跨進程使用,因此沒有必要使用互斥量,只須要使用臨界區就能夠了。假設有一組進程在同時使用一個文件那麼咱們可使用互斥量來保證該文件只同時被一個進程使用(若是隻是利用OS的文件存取控制功能則須要添加更多的錯誤處理代碼),此外在調度程序中也可使用互斥量來對資源的使用進行同步化。安全

相關文章
相關標籤/搜索