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;
}
运行结果如下所示: