C++多线程那些事

线程之间的关系一般有两种,一种是互斥,一种是同步,互斥可以表现为两个线程同时争夺同一个资源,同步可以表现为两个线程按一定次序完成一个任务(如A 完成任务的前半部分,紧接着需要线程B 完成线程的后半部分)


在C++中处理上面两种关系的常用方法是:

关键段、事件、互斥量、信号量。


注意C++开启新的线程一定使用_beginthreadex函数而不要使用CreateThread函数,因为后者对系统中的全局变量没有保护,所以多个线程程环境下,容易出现系统的全局变量的值被覆盖的情况,而前者每个线程都有单独自己的系统的全局变量,例如:errno

另外需要注意,两个方法所需要的线程处理函数的签名不同,_beginthreadex为:

unsigned int __stdcall Fun(void *pPM)【void * 也可以用C++中的宏LPVOID代替,意思是一样的】

一、关键段CRITICAL_SECTION

关键段好像一个房间,只有通过Enter-CriticalSection  这个函数进入这个关键段后,就不再会受到其他线程的打扰(比如在这个关键段中可以访问一个系统资源,那么这个资源就不能被同时被其他线程访问,就是一旦一个线程进入关键段后,其他试图进入这个关键段的线程将被挂起),只有当这个线程使用了Leave-CriticalSection 离开关键段后,其他线程才能访问这个关键段(注意:可以理解为 将两个函数间的代码放到了一个关键段中(在初始化的时候,这个关键段就有名字了,就是房间名),那么其他线程比如等待在这个线程退出关键段后才能进入这个关键段)

#include "stdafx.h"
#include <iostream>  
#include <time.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <windows.h>
#include <process.h>

using namespace std;

// 关键段结构对象(也就是房间名)
CRITICAL_SECTION g_cs;
// 共享资源
char g_cArray[10];

unsigned int __stdcall ThreadProc10(LPVOID pParam)
{
		// 进入关键段
	EnterCriticalSection(&g_cs);
	// 对共享资源进行写入操作

	for(int i=0;i<10;i++)
	{
	g_cArray[i] = 'a';
	Sleep(1);
	}
	// 离开关键段
	LeaveCriticalSection(&g_cs);
	return 0;
}
unsigned int __stdcall ThreadProc11(LPVOID pParam)
{
	// 进入临界区
	EnterCriticalSection(&g_cs);
	// 对共享资源进行写入操作
	for(int i=0;i<10;i++)
	{
	g_cArray[10-i-1] = 'b';
	Sleep(1);
	}http://write.blog.csdn.net/postedit
	// 离开临界区
	LeaveCriticalSection(&g_cs);
	return 0;
}

int main()
{
//	cout << "Armadillo version: " << arma_version::as_string() << endl;
	// 初始化关键段
	InitializeCriticalSection(&g_cs);
	// 启动线程
	_beginthreadex(NULL, 0, ThreadProc10, NULL, 0, NULL);
	_beginthreadex(NULL, 0, ThreadProc11, NULL, 0, NULL);

	Sleep(300);
	for(int i=0;i<10;i++)
	{
	 cout<<g_cArray[i]<<endl;
	}
} 

我们可以看到,因为两个线程中使用的函数都进入了同一个关键段,那么当只有当第一个线程中使用的函数离开关键段以后,第二个线程中的函数才能进入关键段,就把这个char数组中的指全部写成了b,也就是我们看到的结果

【关键段使用很简单,就是定义关键段,初始化关键段,进入关键段,退出关键段】

二、事件Event

事件相当于一个门,使用SetEvent 就可以打开这个门,然后就可以让一个线程进入,使用ResetEvent  将这门关上,门关上以后,其他线程就只能等着了。使用WaitForSingleObject 来等待这个门的开启,一旦门被其他线程使用SetEvent 开启之后,那个这个线程就可以通过WaitForSingleObject 执行之后的代码,在通过WaitForSingleObject 这个函数的时候,这个函数会自动调用ResetEvent  把门关上,让其他线程等待

#include "stdafx.h"
#include <process.h>  
#include <windows.h> 
#include<iostream>
using namespace std;
// 事件句柄
HANDLE hEvent = NULL;
// 共享资源
char g_cArray[10];

unsigned int __stdcall ThreadProc12(LPVOID pParam)
{
	// 等待事件置位
	WaitForSingleObject(hEvent, INFINITE);
	// 对共享资源进行写入操作
	for (int i = 0; i < 10; i++)
	{
	   g_cArray[i] = 'a';
		Sleep(10);
	}
	// 处理完成后即将事件对象置位
	SetEvent(hEvent);
	return 0;
}
unsigned int __stdcall ThreadProc13(LPVOID pParam)
{
	// 等待事件置位
	WaitForSingleObject(hEvent, INFINITE);
	// 对共享资源进行写入操作
	for (int i = 0; i < 10; i++)
	{
	 g_cArray[10 - i - 1] = 'b';
	 Sleep(10);
	}
	// 处理完成后即将事件对象置位
	SetEvent(hEvent);
	return 0;
}
void main()
{
	// 创建事件
	hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
	// 事件置位
	SetEvent(hEvent);
	// 启动线程
	_beginthreadex(NULL, 0, ThreadProc12, NULL, 0, NULL);
	_beginthreadex(NULL, 0, ThreadProc13, NULL, 0, NULL);
	// 等待计算完毕
	Sleep(3000);
	for(int i=0;i<10;i++)
	{
		cout<<g_cArray[i]<<endl;
	}
}
这个代码产生的效果和上面关键段的代码是一样的

第一个 CreateEvent

函数功能:创建事件

函数原型:

HANDLECreateEvent(

 LPSECURITY_ATTRIBUTESlpEventAttributes,

 BOOLbManualReset,

 BOOLbInitialState,

 LPCTSTRlpName

);

函数说明:

第一个参数表示安全控制,一般直接传入NULL

第二个参数确定事件是手动置位还是自动置位,传入TRUE表示手动置位,传入FALSE表示自动置位。如果为自动置位,则对该事件调用WaitForSingleObject()后会自动调用ResetEvent()使事件变成未触发状态。打个小小比方,手动置位事件相当于教室门,教室门一旦打开(被触发),所以有人都可以进入直到老师去关上教室门(事件变成未触发)。自动置位事件就相当于医院里拍X光的房间门,门打开后只能进入一个人,这个人进去后会将门关上,其它人不能进入除非门重新被打开(事件重新被触发)。

第三个参数表示事件的初始状态,传入TRUR表示已触发。

第四个参数表示事件的名称,传入NULL表示匿名事件。

使用临界区只能同步同一进程中的线程,而使用事件内核对象则可以对进程外的线程进行同步,其前提是得到对此事件对象的访问权。可以通过OpenEvent()函数获取得到

如果需要在一个线程中等待多个事件,则用WaitForMultipleObjects()来等待。WaitForMultipleObjects()与WaitForSingleObject()类似,同时监视位于句柄数组中的所有句柄。这些被监视对象的句柄享有平等的优先权,任何一个句柄都不可能比其他句柄具有更高的优先权。WaitForMultipleObjects()的函数原型为:

DWORD WaitForMultipleObjects(
 DWORD nCount, // 等待句柄数
 CONST HANDLE *lpHandles, // 句柄数组首地址
 BOOL fWaitAll, // 等待标志
 DWORD dwMilliseconds // 等待时间间隔
);


三、互斥量Mutex

互斥量也具有“进程拥有权”的概念,也就是说只有互斥量只能使用在一个进程中的多线线程中的互斥问题,实际上互斥量和关键段的效果是差不多的

互斥量和关键段的不同

所以它比关键段还多一个很有用的特性——“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将公平地选定一个等待线程来完成调度(被选中的线程的WaitForSingleObject()会返回WAIT_ABANDONED_0)。


#include "stdafx.h"
#include <process.h>  
#include <windows.h> 
#include<iostream>
using namespace std;

// 互斥对象
HANDLE hMutex = NULL;
char g_cArray[10];
unsigned int __stdcall ThreadProc18(LPVOID pParam)
{
	// 等待互斥对象通知
	WaitForSingleObject(hMutex, INFINITE);
	// 对共享资源进行写入操作
	for (int i = 0; i < 10; i++)
	{
		g_cArray[i] ='a';
		Sleep(1);
	}
	// 释放互斥对象
	ReleaseMutex(hMutex);
	return 0;
}
unsigned int __stdcall ThreadProc19(LPVOID pParam)
{
	// 等待互斥对象通知
	WaitForSingleObject(hMutex, INFINITE);
	// 对共享资源进行写入操作
	for (int i = 0; i < 10; i++)
	{
		g_cArray[10 - i - 1] = 'b';
		Sleep(1);
	}
	// 释放互斥对象
	ReleaseMutex(hMutex);
	return 0;
}

void main()
{
	// 创建互斥对象
	hMutex = CreateMutex(NULL, FALSE, NULL);


	// 启动线程
	_beginthreadex(NULL, 0, ThreadProc18, NULL, 0, NULL);
	_beginthreadex(NULL, 0, ThreadProc19, NULL, 0, NULL);
	// 等待计算完毕
	Sleep(3000);
	for(int i=0;i<10;i++)
	{
		cout<<g_cArray[i]<<endl;
	}
}
效果还是上面程序
在编写程序时,互斥对象多用在对那些为多个线程所访问的内存块的保护上,可以确保任何线程在处理此内存块时都对其拥有可靠的独占访问权。

HANDLE CreateMutex(
 LPSECURITY_ATTRIBUTES lpMutexAttributes, // 安全属性指针
 BOOL bInitialOwner, // 初始拥有者
 LPCTSTR lpName // 互斥对象名
);

参数bInitialOwner主要用来控制互斥对象的初始状态。一般多将其设置为FALSE,以表明互斥对象在创建时并没有为任何线程所占有。如果在创建互斥对象时指定了对象名,那么可以在本进程其他地方或是在其他进程通过OpenMutex()函数得到此互斥对象的句柄。OpenMutex()函数原型为:

HANDLE OpenMutex(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 互斥对象名
);

当目前对资源具有访问权的线程不再需要访问此资源而要离开时,必须通过ReleaseMutex()函数来释放其拥有的互斥对象

四、信号量Semaphore

信号量(Semaphore)内核对象对线程的同步方式与前面几种方法不同,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。在用CreateSemaphore()创建信号量时即要同时指出允许的最大资源计数和当前可用资源计数。一般是将当前可用资源计数设置为最大资源计数,每增加一个线程对共享资源的访问,当前可用资源计数就会减1,只要当前可用资源计数是大于0的,就可以发出信号量信号。但是当前可用计数减小到0时则说明当前占用资源的线程数已经达到了所允许的最大数目,不能在允许其他线程的进入,此时的信号量信号将无法发出。线程在处理完共享资源后,应在离开的同时通过ReleaseSemaphore()函数将当前可用资源计数加1。在任何时候当前可用资源计数决不可能大于最大资源计数。

HANDLE CreateSemaphore(
 LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, // 安全属性指针
 LONG lInitialCount, // 初始计数
 LONG lMaximumCount, // 最大计数
 LPCTSTR lpName // 对象名指针
);

  参数lMaximumCount是一个有符号32位值,定义了允许的最大资源计数,最大取值不能超过4294967295。lpName参数可以为创建的信号量定义一个名字,由于其创建的是一个内核对象,因此在其他进程中可以通过该名字而得到此信号量。OpenSemaphore()函数即可用来根据信号量名打开在其他进程中创建的信号量,函数原型如下:
HANDLE OpenSemaphore(
 DWORD dwDesiredAccess, // 访问标志
 BOOL bInheritHandle, // 继承标志
 LPCTSTR lpName // 信号量名
);
  在线程离开对共享资源的处理时,必须通过ReleaseSemaphore()来增加当前可用资源计数。否则将会出现当前正在处理共享资源的实际线程数并没有达到要限制的数值,而其他线程却因为当前可用资源计数为0而仍无法进入的情况。ReleaseSemaphore()的函数原型为:
BOOL ReleaseSemaphore(
 HANDLE hSemaphore, // 信号量句柄
 LONG lReleaseCount, // 计数递增数量
 LPLONG lpPreviousCount // 先前计数
);
  该函数将lReleaseCount中的值添加给信号量的当前资源计数,一般将lReleaseCount设置为1,如果需要也可以设置其他的值。WaitForSingleObject()和WaitForMultipleObjects()主要用在试图进入共享资源的线程函数入口处,主要用来判断信号量的当前可用资源计数是否允许本线程的进入。只有在当前可用资源计数值大于0时,被监视的信号量内核对象才会得到通知。


我了更好的理解信号量,我们来看看哲学家就餐问题

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。
哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。
在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。

解决方案:

在哲学家就餐问题中,资源(餐叉)按照某种规则编号为1至5,每一个工作单元(哲学家)总是先拿起左右两边编号较低的餐叉,再拿编号较高的。用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。在这种情况下,当四位哲学家同时拿起他们手边编号较低的餐叉时,只有编号最高的餐叉留在桌上,从而第五位哲学家就不能使用任何一只餐叉了。而且,只有一位哲学家能使用最高编号的餐叉,所以他能使用两只餐叉用餐。当他吃完后,他会先放下编号最高的餐叉,再放下编号较低的餐叉,从而让另一位哲学家拿起后边的这只开始吃东西。

尽管资源分级能避免死锁,但这种策略并不总是实用的,特别是当所需资源的列表并不是事先知道的时候。例如,假设一个工作单元拿着资源3和5,并决定需要资源2,则必须先要释放5,之后释放3,才能得到2,之后必须重新按顺序获取3和5。对需要访问大量数据库记录的计算机程序来说,如果需要先释放高编号的记录才能访问新的记录,那么运行效率就不会高,因此这种方法在这里并不实用。
这种方法经常是实际计算机科学问题中最实用的解法,通过为分级锁指定常量,强制获得锁的顺序,就可以解决这个问题。
//经典线程同步问题 互斥量Mutex
#include "stdafx.h"
#include <windows.h>
#include <process.h>
#include <time.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <iostream>

using namespace std;                     //命名空间std内定义的所有标识符都有效

const unsigned int PHILOSOPHER_NUM=5;    //哲学家数目
const char THINKING=1;                   /*标记当前哲学家的状态,1表示等待,2表示得到饥饿,3表示正在吃饭*/
const char HUNGRY=2;
const char DINING=3;

HANDLE hPhilosopher[5];                  //定义数组存放哲学家
/*HANDLE(句柄)是windows操作系统中的一个概念。指的是一个核心对象在某一个进程中的唯一索引*/

HANDLE semaphore[PHILOSOPHER_NUM];       // semaphore 用来表示筷子是否可用

HANDLE mutex;                            // Mutex用来控制安全输出

DWORD WINAPI philosopherProc( LPVOID lpParameter)       //返回 DWORD(32位数据)的 API 函数philosopherProc
{
	int  myid;
	char idStr[128];
	char stateStr[128];
	char mystate;
	int  ret;

	unsigned int leftFork;                  //左筷子
	unsigned int rightFork;                 //右筷子

	myid = int(lpParameter);
	itoa(myid, idStr, 10);


	WaitForSingleObject(mutex, INFINITE);
	cout << "philosopher " << myid << " begin......" << endl;
	ReleaseMutex(mutex);


	mystate = THINKING;                                      //初始状态为THINKING
	leftFork = (myid) % PHILOSOPHER_NUM;
	rightFork = (myid + 1) % PHILOSOPHER_NUM;

	while (true)
	{
		switch(mystate)
		{
		case THINKING:

			mystate = HUNGRY;                                      // 改变状态
			strcpy(stateStr, "HUNGRY"); 
			break;

		case HUNGRY:
			strcpy(stateStr, "HUNGRY");

			ret = WaitForSingleObject(semaphore[leftFork], 0);    // 先检查左筷子是否可用
			if (ret == WAIT_OBJECT_0)
			{   
				ret = WaitForSingleObject(semaphore[rightFork], 0);  //左筷子可用就拿起,再检查右筷子是否可用 
				if (ret == WAIT_OBJECT_0)     //如果信号量已通知 可以调度
				{
					mystate = DINING;                                   // 右筷子可用,就改变自己的状态
					strcpy(stateStr, "DINING");
				}
				else
				{
					ReleaseSemaphore(semaphore[leftFork], 1, NULL);     // 如果右筷子不可用,就把左筷子放下
				}
			}
			break;

		case DINING:
			// 吃完后把两支筷子都放下
			ReleaseSemaphore(semaphore[leftFork], 1, NULL);//递增信号量的当前资源计数
			ReleaseSemaphore(semaphore[rightFork], 1, NULL);

			mystate = THINKING;                                   // 改变自己的状态
			strcpy(stateStr, "THINKING");
			break;
		}

		// 输出状态
		WaitForSingleObject(mutex, INFINITE);
		cout << "philosopher " << myid << " is : " << stateStr << endl;  
		ReleaseMutex(mutex);
		// sleep a random time : between 1 - 5 s
		int sleepTime; 
		sleepTime = 1 + (int)(5.0*rand()/(RAND_MAX+1.0)); 
		Sleep(sleepTime*10);
	}
}


int main()
{
	int i;
	srand(time(0));

	mutex = CreateMutex(NULL, false, NULL);//第二个参数如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。
	for (i=0; i<PHILOSOPHER_NUM; i++)
	{
		semaphore[i] = CreateSemaphore(NULL, 1, 1, NULL);//第二个参数表示初始资源数量 第三个参数表示最大并发数量 		
		hPhilosopher[i]=CreateThread(NULL,0,philosopherProc,LPVOID(i), CREATE_SUSPENDED,0);  //CREATE_SUSPENDED则表示线程创建后暂停运行 直到调用ResumeThread()
	}
	for (i=0; i<PHILOSOPHER_NUM; i++)
		ResumeThread(hPhilosopher[i]);
	Sleep(2000);
	return 0;
}

/*当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽故信号量处于末触发。
在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,
如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。
*/


C++多线程那些事,布布扣,bubuko.com

C++多线程那些事

上一篇:【leetcode】Reverse Words in a String (python)


下一篇:Effective C++:条款38:通过复合塑模出has-a或“根据某物实现出”