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 
#include 
#include 

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;
}

运行结果截图如下:


C++多线程系列(二)线程互斥_第1张图片

从运行结果可知,理论上来讲线程访问两个全局变量,其输出结果应该相同,者是因为同时访问g_nCount1和g_nCount2的两个线程具有相同的优先级,在执行过程中如果第一个线程取走g_nCount1的值准备进行自加操作的时候,他的时间敲好用完,系统切换到第二个线程去对g_nCount1进行自加操作,在一个时间片后第一个线程再次被调用,此事它会去除上次的值自加而非第二个线程自加后的值,这样值就会覆盖第二个线程操作得到的值。同样g_nCount2也存在相同的问题。

在添加临界区对象后,这种情况就不复存在了。如下

#include 
#include 
#include 

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;
}


运行结果如下:

C++多线程系列(二)线程互斥_第2张图片

感觉这个例子不是太好呢,下个函数换个典型的例子!!!!

总结:临界区的存在保证了多线程在同一时间只能有一个访问共享资源,保证了数据的一致性!


(2)互斥量mutex


互斥量是一个内核对象,用来确保一个线程独占一个资源的访问。互斥量与关键段行为非常相似,而且互斥量可以用于不同进程中的线程互斥访问资源。在C++11中与mutex相关的类(包括锁类型)和函数都声明在头文件中,如果需要使用std::mutex,就必须包含头文件。而在标准C开发中则不需要包含头文件,可以使用CreateMutex函数创建互斥量。C++11中包含四中类型有基本的mutex、递归mutex类、定时mutex类和定时递归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 
#include 
#include 

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;
}


运行结果如下:

C++多线程系列(二)线程互斥_第3张图片

现在还未涉及到内核事件,如果添加上内核事件代码(及代码中注释掉部分),其线程编号则会保证唯一性。如图所示:

C++多线程系列(二)线程互斥_第4张图片


更改示例代码,其不添加互斥量源代码如下:
#include 
#include 
#include 

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;
}

大家可自己写一遍运行观察添加互斥量后的效果。

参考博客: http://blog.csdn.net/column/details/killthreadseries.html
参考博客: http://www.cnblogs.com/haippy/p/3284540.html


***************************************************************20160925更新********************************************************************
在介绍临界区的那段代码中提到,如果添加内核事件可以保证线程编号的唯一性,但是并不能保证线程编号按顺序输出,所以可以给控制编号的nThreadNum添加上临界区,这样既能保证线程编号的唯一性,又可以保证线程编号顺序输出。代码如下:(代码与之前代码有差异)
#include 
#include 
#include 
#include 

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;
}

其运行结果如图所示:
C++多线程系列(二)线程互斥_第5张图片

使用CRITICAL_SECTION解决经典的线程同步互斥问题,只能用于线程的互斥而不能用于同步。而内核事件Event可以解决线程同步问题。
互斥量和临界区非常相似,只有拥有了互斥对象的线程才可以访问共享资源,而互斥对象只有一个,因此可以保证同一时刻有且仅有一个线程可以访问共享资源,达到线程同步的目的。
互斥量相对于临界区更为高级,可以对互斥量进行命名,支持跨进程同步。互斥量是调用Win32API对互斥锁的操作,因此在同一个操作系统下不同进程可以按照互斥锁的名称共享锁。
正因为如此,互斥锁的操作会更耗资源,性能上相对于临界区也有降低,在使用时还要从多方面考虑,对于进程内的线程同步使用临界区性能会更佳。
参考博文: http://www.cnblogs.com/oneheart/archive/2016/07/01/5633842.html

PS:网上的好资源真的是太多了



你可能感兴趣的:(C\C++)