由於進程/線程間的操作是並行進行的,所以就產生了一個數據的問題同步,我們先看一段代碼:
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的文件存取控制功能則需要添加更多的錯誤處理代碼),此外在調度程序中也可以使用互斥量來對資源的使用進行同步化。