您的位置:首页 > 编程语言 > C语言/C++

C++多线程系列(二)线程互斥

2016-06-30 20:23 1286 查看
首先了解一下线程互斥的概念,线程互斥说白了就是在进程中多个线程的相互制约,如线程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;
}


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

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

***************************************************************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对互斥锁的操作,因此在同一个操作系统下不同进程可以按照互斥锁的名称共享锁。
正因为如此,互斥锁的操作会更耗资源,性能上相对于临界区也有降低,在使用时还要从多方面考虑,对于进程内的线程同步使用临界区性能会更佳。
参考博文:http://www.cnblogs.com/oneheart/archive/2016/07/01/5633842.html

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


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息