Windows中的线程同步

Windows操作系统的运行方式是“双模式操作”:用户模式、内核模式。

用户模式:运行应用程序的基本模式,禁止访问物理设备,而且会限制访问的内存区域。(应用程序的运行模式)

内核模式:操作系统运行时的模式,不仅不会限制访问的内存区域,而且访问的硬件设备也不会受到限制。(操作系统的运行模式)

实际操作中,Windows不会一直处于用户模式,而是在用户模式和内核模式之间切换。例如创建线程的是操作系统,所以创建线程的过程中无法避免向内核模式切换。

用户模式同步:速度快但是有一定的局限性。

内核模式同步:比用户模式同步提供的功能更多;同时可以指定超时,防止产生死锁。

基于用户模式下的同步(CRITICAL_SECTION)

该情况下的同步中将创建并运用CRITICAL_SECTION对象,它是一把进入临界区的钥匙,因此,为了进入临界区需要得到CRITICAL_SECTION这把钥匙。相反要离开临界区则要上交这把钥匙,下面介绍CS对象的初始化以及销毁相关函数

#include<windows.h>

void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

lpCriticalSection Init函数中传入需要初始化的CRITICAL_SECTION对象的地址值,反之Delete函数传入需要接触的CRITICAL_SECTION对象的地址值。

下面介绍获取以及释放CS对象的函数,可以简单理解为获取和释放钥匙的函数

#include<windows.h>

void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);

其中lpCriticalSection是CS的地址值。

这部分与Linux中的互斥量类似,接下来我们就直接写出示例程序:

#include<iostream>
#include<windows.h>
#include<process.h>	//线程库
using namespace std;

unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);

int num,sum=0;
CRITICAL_SECTION cs;

int main()
{
	HANDLE handles[100];
	int i;
	
	InitializeCriticalSection(&cs);
	for(i=0;i<10;i++){
		if(i%2==0)
			handles[i]=(HANDLE)_beginthreadex(NULL,0,read,NULL,0,NULL);    //创建线程
		else
			handles[i]=(HANDLE)_beginthreadex(NULL,0,accu,NULL,0,NULL);     //创建求和线程
	}	
	
	WaitForMultipleObjects(10,handles,TRUE,INFINITE);
	DeleteCriticalSection(&cs);
	return 0;
} 

unsigned WINAPI read(void *arg)
{
	EnterCriticalSection(&cs);
	cout<<"Input num:";
	cin>>num;
	LeaveCriticalSection(&cs);
	return 0;
}

unsigned WINAPI accu(void *arg)
{
	EnterCriticalSection(&cs);
	sum+=num;
	cout<<"sum = "<<sum<<endl;
	LeaveCriticalSection(&cs);
	return 0;
}

程序运行后如下图所示:

 

基于内核模式下的同步方法

典型的内核同步方法有基于时间、信号量、互斥量等内核对象的同步,下面将逐一介绍。

基于互斥量(Mutal Exclusion)对象的同步

基于互斥量对象的同步方法与基于CS对象的同步方法类似,因此互斥量对象同样可以理解为钥匙。首先介绍创建互斥量对象的函数。

#include<windows.h>

HANDLE CreateMutex(

     LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpName);

lpMutexAttributes 传递安全相关的配置信息,使用默认安全设置是可以传递NULL。

bInitialOwner 如果为TRUE,则创建的互斥量对象属于调用该函数的线程,同时进入non-signaled状态,如果为FALSE,则创建的互斥量对象不属于任何线程,此时状态为signaled。

lpName 用于命名互斥量对象。传入NULL时创建无名的互斥量对象。

从上述参数中可以看出,如果互斥量对象不属于任何拥有这,则将进入signaled状态,利用该特点进行同步。另外互斥量属于内核对象,所以通过如下函数销毁,销毁。

BOOL CloseHandle(HANDLE hObject);

hObject是要销毁的内核对象的句柄

BOOL ReleaseMutex(HANDLE hMutex);

hMutex是需要释放(解除拥有)的互斥量对象句柄

 接下来分析获取和释放互斥量的过程。互斥量被某一线程获取时(拥有时)为non-signaled状态,释放时(未拥有时)进入signaled状态。因此,可以使用WaitForSingleObject函数验证互斥量是否已经分配。该函数的调用结果有如下两种:

调用后进入阻塞状态:互斥量对象已经被其他线程获取,现在处于non-signaled状态。

调用后直接返回:其他线程为占用互斥量对象,现处于signaled状态。

互斥量在WaitForSingleObject函数返回时自动进入non-signaled状态。

所以实际操作避免死锁的情况如下所示:

WaitForSingleObject(hMutex,INFINITE);

//临界区开始

..................

//临界区的结束

ReleaseMutex(hMutex);

ReleaseMutex函数使互斥量重新进入signaled状态,所以相当于临界区的出口。

接下来给出内核模式中互斥量同步的示例程序如下:

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

unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);
unsigned int num;

int sum=0;

HANDLE hMutex; 
int main()
{
	HANDLE handles[100];
	int i;
	
	hMutex=CreateMutex(NULL,FALSE,NULL);
	for(i=0;i<10;i++)
	{
		if(i%2==0)
			handles[i]=(HANDLE)_beginthreadex(NULL,0,read,NULL,0,NULL);
		else
			handles[i]=(HANDLE)_beginthreadex(NULL,0,accu,NULL,0,NULL);
	}
	
	WaitForMultipleObjects(10,handles,TRUE,INFINITE);
	CloseHandle(hMutex);
	return 0;
} 

unsigned WINAPI read(void *arg)
{
	WaitForSingleObject(hMutex,INFINITE);
	cout<<"Input num:";
	cin>>num;
	ReleaseMutex(hMutex);
	return 0;
}

unsigned WINAPI accu(void *arg)
{
	WaitForSingleObject(hMutex,INFINITE);
	sum+=num;
	cout<<"sum:"<<sum<<endl;
	ReleaseMutex(hMutex);
	return 0;
}

程序运行后的代码如下所示:

 基于信号量对象的同步

Windows中基于信号量对象的同步与linux中的信号量类似,二者都是利用名为“信号量值”的整数值来完成同步的,而且该值都不能小于0。

下面介绍创建信号量的对象函数,销毁同样是利用CloseHandle函数。

HANDLE CreateSemaphore(

        LPSECURITY_ATTRIBUTES lpMutexAttributes,LONG lInitialCount,LONG lMaximumCount,LPCTSTR lpName);

lpMutexAttributes 传递安全相关的配置信息,使用默认安全设置是可以传递NULL。

lInitialCount 指定信号量的出迟滞,应大于0小于lMaximumCount。

lMaximumCount 信号量的最大值。改值为1时,信号量变为只能表示0和1的二进制信号量。

lpName 用于命名互斥量对象。传入NULL时创建无名的互斥量对象。

可以利用信号量值为0时进入non-signaled状态,大于0时进入signaled状态的特性进行同步。向lInitialCount参数传递0时,创建non-signaled状态的信号量对象。而向lMaximumCount传入3时,信号量最大值时3,因此可以实现3个线程同时访问临界区的同步。下面介绍释放信号量对象的函数。

BOOL ReleaseSemaphore(HANDLE hSemaphore,LONG lReleaseCount,LPLONG lpPreviousCount);

hSemaphore 传递需要释放的信号量对象

IRleaseCount 释放意味着信号量的增加,通过该参数可以指定增加的值。超过最大值则不增加,返回FLASE

lpPreviousCoun t 用于保存修改之前值的变量地址,不需要时可以传递NULL。

所以实际操作避免死锁的情况如下所示:

WaitForSingleObject(hSemaphore,INFINITE);

//临界区的开始

.......................

//临界区的结束

ReleaseSemaphore(hSemaphore,1,NULL)

下面给出基于信号量同步的示例程序以及最终显示结果:

#include<iostream>
#include<windows.h>
#include<process.h>
using namespace std;
unsigned WINAPI read(void *arg);
unsigned WINAPI accu(void *arg);

static HANDLE semone;
static HANDLE semtwo;
static int num;

int main()
{
	HANDLE hThread1,hThread2;
	semone=CreateSemaphore(NULL,0,1,NULL);	//设置semone为no-signaled状态,让线程main函数进行等待(accu) 
	semtwo=CreateSemaphore(NULL,1,1,NULL);	//设置semtwo为signaled状态,让线程main函数(read)运行不再等待 
	
	//创建线程 
	hThread1=(HANDLE)_beginthreadex(NULL,0,read,NULL,0,NULL);
	hThread2=(HANDLE)_beginthreadex(NULL,0,accu,NULL,0,NULL);
	
	WaitForSingleObject(hThread1,INFINITE);
	WaitForSingleObject(hThread2,INFINITE);
	
	CloseHandle(semone);
	CloseHandle(semtwo);
	return 0;
}

unsigned WINAPI read(void *arg)
{
	int i;
	for(i=0;i<5;i++)
	{
		cout<<"Input num:";
		WaitForSingleObject(semtwo,INFINITE);	//当semtwo为signaled时运行剩余部分,让值-1变为no-signaled状态 
			cin>>num;
		ReleaseSemaphore(semone,1,NULL);	//给semone+1即将其设置为signaled状态让accu运行 
	}
	return 0;
}

unsigned WINAPI accu(void *arg)
{
	int sum=0,i;
	for(i=0;i<5;i++)
	{
		WaitForSingleObject(semone,INFINITE);	//当semone为signaled时运行剩余部分,并让其值-1变为no-signaled状态继续进行等待 
		sum+=num;
		ReleaseSemaphore(semtwo,1,NULL);
	}
	cout<<"result:"<<sum<<endl;
	return 0;
}

示例程序运行结果如下所示:                                                                     

 基于事件对象的同步:

事件同步对象与前2种同步方法相比有很大不同,区别就在于,该方式下创建对象时,可以在自动以non-signaled状态运行的auto-reset模式和与之相反的manual-reset模式中任选其一。而事件对象的主要特点是可以创建manual-reset模式的对象。首先介绍创建事件对象的函数。

HANDLE CreateEvent(

        LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bManualReset,BOOL bInitialState,LPCTSTR lpName);

成功时返回事件对象句柄,失败时返回NULL

lpMutexAttributes 传递安全相关的配置信息,使用默认安全设置是可以传递NULL。

bManualReset 传入TRUE时创建manual-reset模式的事件对象,传入FALSE时创建auto-reset模式的事件对象。

bInitialState 传入TRUE时创建signaled状态的事件对象,反之创建non-signaled状态的事件对象

lpName 用于命名互斥量对象。传入NULL时创建无名的互斥量对象。

下面可以通过函数可以更改对象状态:

BOOL ResetEvent(HANDLE hEvent);        //to the non-signaled

BOOL SetEvent(HANDLE hEvent);        //to the signaled

下面该实例中将介绍事件对象的具体使用方法,该示例中的2个线程将同时等待输入字符串。

#include<stdio.h>
#include<windows.h>
#include<process.h>
#define STR_LEN 100

unsigned WINAPI NumberofA(void *arg);
unsigned WINAPI NumberofOthers(void *arg);

static char str[STR_LEN];
static HANDLE hEvent;

int main()
{
	HANDLE hthread1,hthread2;
	hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
	hthread1=(HANDLE)_beginthreadex(NULL,0,NumberofA,NULL,0,NULL);
	hthread2=(HANDLE)_beginthreadex(NULL,0,NumberofOthers,NULL,0,NULL);
	
	fputs("Input string:",stdout);
	fgets(str,STR_LEN,stdin);
	SetEvent(hEvent);	//读取字符串后将事件对象设置为signaled终止状态。
	
	WaitForSingleObject(hthread1,INFINITE);
	WaitForSingleObject(hthread2,INFINITE);	//等待其全部成为signaled
	ResetEvent(hEvent);	//将其设置为non-signaled
	CloseHandle(hEvent);
	return 0; 
}

unsigned WINAPI NumberofA(void *arg)
{
	int i,cnt=0;
	WaitForSingleObject(hEvent,INFINITE);
	for(i=0;str[i]!=0;i++)
	{
		if(str[i]=='A')
			cnt++;
	}
	printf("Num of A: %d\n",cnt);
	return 0;
}

unsigned WINAPI NumberofOthers(void *arg)
{
	int i,cnt2=0;
	WaitForSingleObject(hEvent,INFINITE);
	for(i=0;str[i]!=0;i++)
	{
		if(str[i]!='A')
			cnt2++;
	}
	printf("Num of others: %d\n",cnt2-1);
	return 0;
}

运行结果如下所示: 

猜你喜欢

转载自blog.csdn.net/m0_61886762/article/details/129641199