1. 程式人生 > >C++多執行緒系列(二)執行緒互斥

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:網上的好資源真的是太多了