C++多執行緒系列(二)執行緒互斥
首先了解一下執行緒互斥的概念,執行緒互斥說白了就是在程序中多個執行緒的相互制約,如執行緒A未執行完畢,其他執行緒就需要等待!
執行緒之間的制約關係分為間接相互制約和直接相互制約。
所謂間接相互制約:一個系統中的多個執行緒必然要共享某種系統資源如共享CPU,共享印表機。間接制約即源於資源共享,執行緒A在列印的時候其他執行緒就要等待,否則列印的資料將變得非常混亂。間接相互制約稱為互斥,互斥是同步的一種特殊形式
直接相互制約:主要指的是執行緒之間的一種遞進關係,例如執行緒B執行的條件之一是需要執行緒A提供的引數,那麼線上程A將資料傳到執行緒B之前,執行緒B都將處於阻塞狀態,稱為同步。以後再說
(1)臨界區,有的稱為關鍵段,是定義在資料段中的一個CRITICAL_SECTION結構,確保在同一時間只有一個執行緒訪問該資料段中的資料。計算機中大多數物理裝置,程序中的共享變數等都是臨界資源,它們要求被互斥訪問,每個程序中訪問的臨界資源的程式碼稱為臨界區
寫程式碼的時候可通過
CRITICAL_SECTION g_csThreadCode;
對臨界區進行定義,但是在使用臨界區之前首先要對臨界區物件進行初始化,其函式原型如下:
InitializeCriticalSection(
_Out_ LPCRITICAL_SECTION lpCriticalSection
);
對臨界區物件初始化完成後,執行緒訪問臨界區資料必須首先呼叫EnterCriticalSection函式申請進入臨界區。在同一時間內,Windows只允許一個執行緒進入臨界區。所以在申請的時候,如果有另一個執行緒在臨界區的話,EnterCriticalSection函式將會一直等待下去,知道其他執行緒離開臨界區才返回。EnterCriticalSection函式定義如下:
EnterCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
當臨界區物件操作完成後使用函式LeaveCriticalSection函式離開臨界區,將臨界區交還給Windows方便其他執行緒繼續申請使用
LeaveCriticalSection(
_Inout_ LPCRITICAL_SECTION lpCriticalSection
);
用完臨界區物件,使用DeleteCriticalSection函式將物件刪除
DeleteCriticalSection( _Inout_ LPCRITICAL_SECTION lpCriticalSection );
例:用執行緒同時訪問全域性變數並對全域性變數進行操作,如果不使用臨界區訪問,程式碼如下
#include <stdio.h>
#include <Windows.h>
#include <process.h>
int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;
UINT _stdcall ThreadFun(LPVOID);
int main(int argc, char *argv[])
{
HANDLE threads[2];
threads[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);
threads[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);
//等待1秒,結束兩個計數執行緒,關閉控制代碼
Sleep(1000);
g_bContinue = FALSE;
WaitForMultipleObjects(2, threads, TRUE, INFINITE);
CloseHandle(threads[0]);
CloseHandle(threads[1]);
printf("g_nCount1 = %d\n", g_nCount1);
printf("g_nCount2 = %d\n", g_nCount2);
return 0;
}
UINT _stdcall ThreadFun(LPVOID)
{
while (g_bContinue)
{
g_nCount1++;
g_nCount2++;
}
return 0;
}
執行結果截圖如下:
從執行結果可知,理論上來講執行緒訪問兩個全域性變數,其輸出結果應該相同,者是因為同時訪問g_nCount1和g_nCount2的兩個執行緒具有相同的優先順序,在執行過程中如果第一個執行緒取走g_nCount1的值準備進行自加操作的時候,他的時間敲好用完,系統切換到第二個執行緒去對g_nCount1進行自加操作,在一個時間片後第一個執行緒再次被呼叫,此事它會去除上次的值自加而非第二個執行緒自加後的值,這樣值就會覆蓋第二個執行緒操作得到的值。同樣g_nCount2也存在相同的問題。
在新增臨界區物件後,這種情況就不復存在了。如下
#include <stdio.h>
#include <Windows.h>
#include <process.h>
int g_nCount1 = 0;
int g_nCount2 = 0;
BOOL g_bContinue = TRUE;
CRITICAL_SECTION g_csThread; //宣告臨界區物件
UINT _stdcall ThreadFun(LPVOID);
int main(int argc, char *argv[])
{
InitializeCriticalSection(&g_csThread); //初始化臨界區物件
HANDLE threads[2];
threads[0] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);
threads[1] = (HANDLE)_beginthreadex(NULL, 0, ThreadFun, NULL, 0, NULL);
//等待1秒,結束兩個計數執行緒,關閉控制代碼
Sleep(1000);
g_bContinue = FALSE;
WaitForMultipleObjects(2, threads, TRUE, INFINITE);
CloseHandle(threads[0]);
CloseHandle(threads[1]);
DeleteCriticalSection(&g_csThread); //刪除臨界區物件
printf("g_nCount1 = %d\n", g_nCount1);
printf("g_nCount2 = %d\n", g_nCount2);
return 0;
}
UINT _stdcall ThreadFun(LPVOID)
{
while (g_bContinue)
{
EnterCriticalSection(&g_csThread); //申請進入臨界區
g_nCount1++;
g_nCount2++;
LeaveCriticalSection(&g_csThread); //離開臨界區
}
return 0;
}
執行結果如下:
感覺這個例子不是太好呢,下個函式換個典型的例子!!!!
總結:臨界區的存在保證了多執行緒在同一時間只能有一個訪問共享資源,保證了資料的一致性!
(2)互斥量mutex
互斥量是一個核心物件,用來確保一個執行緒獨佔一個資源的訪問。互斥量與關鍵段行為非常相似,而且互斥量可以用於不同程序中的執行緒互斥訪問資源。在C++11中與mutex相關的類(包括鎖型別)和函式都宣告在<mutex>標頭檔案中,如果需要使用std::mutex,就必須包含<mutex>標頭檔案。而在標準C開發中則不需要包含<mutex>標頭檔案,可以使用CreateMutex函式建立互斥量。C++11中<mutex>包含四中型別有基本的mutex<std::mutex>、遞迴mutex類<std::recursive_mutex>、定時mutex類<std::time_mutex>和定時遞迴mutex類<std::recursive_timed_mutex>,在這裡只介紹標準C開發中用CreateMutex函式建立互斥量。
首先建立互斥量:CreateMutex,查閱庫函式發現#define CreateMutex CreateMutexW,追根溯源,直接看定義
CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全控制,一般直接傳入NULL表示預設值
_In_ BOOL bInitialOwner, //引數用來確定互斥量的初始擁有者
_In_opt_ LPCWSTR lpName //設定互斥量的名稱,NULL則為匿名互斥量
);
有必要對函式第二個引數單獨進行說明bInitialOwner,互斥量的初始擁有者,如果傳入TRUE表示互斥量物件內部會記錄建立它的執行緒的執行緒ID號並將遞迴計數設定為1,由於該執行緒ID非零,所以互斥量處於未觸發狀態.如果傳入FALSE,那麼互斥量物件內部執行緒ID號將設定為NULL,遞迴計數設定為0,這意味著互斥量不為任何執行緒佔用,處於觸發狀態。
開啟互斥量OpenMutex,檢視庫函式,是OpenMutexW的重定義:#define OpenMutex OpenMutexW
HANDLE
WINAPI
CreateMutexW(
_In_opt_ LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全控制,一般直接傳入NULL表示預設值
_In_ BOOL bInitialOwner, //引數用來確定互斥量的初始擁有者
_In_opt_ LPCWSTR lpName //設定互斥量的名稱,NULL則為匿名互斥量
);
第三個引數lpName表示一個程序中的執行緒建立互斥量後,其它程序中的執行緒就可以通過這個函式來找到這個互斥量。
OpenMutex如果訪問成功則返回一個表示互斥量的控制代碼,如果失敗則返回NULL
觸發互斥量:ReleaseMutex函式,其定義為:
BOOL
WINAPI
ReleaseMutex(
_In_ HANDLE hMutex
);
訪問互斥資源前應該要呼叫等待函式WaitFor***(程式碼中有體現,或Single或Multi),結束訪問時就要使用ReleaseMutex()來表示自己已經結束訪問,其他執行緒可以開始訪問.
示例程式碼:
#include <stdio.h>
#include <process.h>
#include <Windows.h>
long g_nNum;
UINT _stdcall threadFun(LPVOID);
const int threadNum = 10;
HANDLE g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode;
//HANDLE g_hThreadEvent; //宣告核心事件
int main(int argc, char *argv[])
{
g_hThreadParameter = CreateMutex(NULL, FALSE, NULL); //生成mutex互斥量
InitializeCriticalSection(&g_csThreadCode); //初始化臨界區
//g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL); //定義核心事件
HANDLE handle[threadNum];
g_nNum = 0;
int i = 0;
while (i < threadNum)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, threadFun, &i, 0, NULL);
WaitForSingleObject(g_hThreadParameter, INFINITE);
//WaitForSingleObject(g_hThreadEvent, INFINITE);
i++;
}
WaitForMultipleObjects(threadNum, handle, TRUE, INFINITE);
CloseHandle(g_hThreadParameter);
DeleteCriticalSection(&g_csThreadCode);
CloseHandle(handle);
for (i = 0; i < threadNum; i++)
{
CloseHandle(handle[i]);
}
//CloseHandle(g_hThreadEvent);
return 0;
}
UINT _stdcall threadFun(LPVOID pPM)
{
int nThreadNum = *(int *)pPM;
//SetEvent(g_hThreadEvent);
ReleaseMutex(g_hThreadParameter);
Sleep(100);
EnterCriticalSection(&g_csThreadCode);
g_nNum++;
Sleep(0);
printf("執行緒編號為%d 全域性變數為%d\n", nThreadNum, g_nNum);
LeaveCriticalSection(&g_csThreadCode);
return 0;
}
執行結果如下:
現在還未涉及到核心事件,如果新增上核心事件程式碼(及程式碼中註釋掉部分),其執行緒編號則會保證唯一性。如圖所示:
#include <stdio.h>
#include <process.h>
#include <windows.h>
long g_nNum; //全域性資源
unsigned int __stdcall Fun(void *pPM); //執行緒函式
const int THREAD_NUM = 10; //子執行緒個數
int main()
{
g_nNum = 0;
HANDLE handle[THREAD_NUM];
int i = 0;
while (i < THREAD_NUM)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
i++;//等子執行緒接收到引數時主執行緒可能改變了這個i的值
}
//保證子執行緒已全部執行結束
WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
//由於建立執行緒是要一定的開銷的,所以新執行緒並不能第一時間執行到這來
int nThreadNum = *(int *)pPM; //子執行緒獲取引數
Sleep(50);//some work should to do
g_nNum++; //處理全域性資源
Sleep(0);//some work should to do
printf("執行緒編號為%d 全域性資源值為%d\n", nThreadNum, g_nNum);
return 0;
}
大家可自己寫一遍執行觀察新增互斥量後的效果。 ***************************************************************20160925更新******************************************************************** 在介紹臨界區的那段程式碼中提到,如果新增核心事件可以保證執行緒編號的唯一性,但是並不能保證執行緒編號按順序輸出,所以可以給控制編號的nThreadNum新增上臨界區,這樣既能保證執行緒編號的唯一性,又可以保證執行緒編號順序輸出。程式碼如下:(程式碼與之前程式碼有差異)
#include <stdio.h>
#include <iostream>
#include <Windows.h>
#include <process.h>
using namespace std;
long g_nNum;
UINT _stdcall threadFun(LPVOID);
const int threadNum = 10; //生成的子執行緒個數
HANDLE g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode, g_csThreadNum;
//宣告核心事件
HANDLE g_hThreadEvent;
int main()
{
g_hThreadParameter = CreateMutex(NULL, FALSE, NULL); //生成mutex互斥量
//初始化臨界區
InitializeCriticalSection(&g_csThreadCode);
InitializeCriticalSection(&g_csThreadNum);
//初始化核心事件
g_hThreadEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
HANDLE handle[threadNum];
g_nNum = 0;
int i = 0;
while (i < threadNum)
{
handle[i] = (HANDLE)_beginthreadex(NULL, 0, threadFun, &i, 0, NULL);
WaitForSingleObject(g_hThreadParameter, INFINITE);
WaitForSingleObject(g_hThreadEvent,INFINITE);
i++;
}
WaitForMultipleObjects(threadNum, handle, TRUE, INFINITE);
CloseHandle(g_hThreadParameter);
DeleteCriticalSection(&g_csThreadCode);
DeleteCriticalSection(&g_csThreadNum);
//CloseHandle(handle);
for (i = 0; i < threadNum; i++)
{
CloseHandle(handle[i]);
}
CloseHandle(g_hThreadEvent);
return 0;
}
UINT _stdcall threadFun(LPVOID pM)
{
EnterCriticalSection(&g_csThreadNum);
int nThreadNum = *(int*)pM;
SetEvent(g_hThreadEvent);
ReleaseMutex(g_hThreadParameter);
Sleep(100);
EnterCriticalSection(&g_csThreadCode);
g_nNum++;
Sleep(100);
cout << "執行緒ID: " << GetCurrentThreadId() << ", 編號為: " << nThreadNum << "數值為: " << g_nNum << endl;
LeaveCriticalSection(&g_csThreadCode);
LeaveCriticalSection(&g_csThreadNum);
return 0;
}
其執行結果如圖所示: 使用CRITICAL_SECTION解決經典的執行緒同步互斥問題,只能用於執行緒的互斥而不能用於同步。而核心事件Event可以解決執行緒同步問題。 互斥量和臨界區非常相似,只有擁有了互斥物件的執行緒才可以訪問共享資源,而互斥物件只有一個,因此可以保證同一時刻有且僅有一個執行緒可以訪問共享資源,達到執行緒同步的目的。 互斥量相對於臨界區更為高階,可以對互斥量進行命名,支援跨程序同步。互斥量是呼叫Win32API對互斥鎖的操作,因此在同一個作業系統下不同程序可以按照互斥鎖的名稱共享鎖。 正因為如此,互斥鎖的操作會更耗資源,效能上相對於臨界區也有降低,在使用時還要從多方面考慮,對於程序內的執行緒同步使用臨界區效能會更佳。 PS:網上的好資源真的是太多了