同步方法的分类及CRITICAL_SECTION同步
用户模式(User mode)和内核模式(Kernal mode)
Windows操作系统的运行方式是“双模式操作”(Dual-mode Operation),这意味着Windows在运行过程中存在如下2种模式。
□用户模式:运行应用程序的基本模式,禁止访问物理设备,而且会限制访问的内存区域。
□内核模式:操作系统运行时的模式,不仅不会限制访问的内存区域,而且访问的硬件设备也不会受限。
内核是操作系统的核心模块,可以简单定义为如下形式。
□用户模式:应用程序的运行模式。
□内核模式:操作系统的运行模式。
实际上,在应用程序运行过程中,Windows操作系统不会一直停留在用户模式,而是在用户模式和内核模式之间切换。例如,各位可以在Windows中创建线程。虽然创建线程的请求是由应用程序的函数调用完成,但实际创建线程的是操作系统。因此,创建线程的过程中无法避免向内核模式的转换。
定义这2种模式主要是为了提高安全性。应用程序的运行时错误会破坏操作系统及各种资源。特别是C/C++可以进行指针运算,很容易发生这类问题。例如,因为错误的指针运算覆盖了操作系统中存有重要数据的内存区域,这很可能引起操作系统崩溃。但实际上各位从未经历过这类事件,因为用户模式会保护与操作系统有关的内存区域。因此,即使遇到错误的指针运算也仅停止应用程序的运行,而不会影响操作系统。
总之,像线程这种伴随着内核对象创建的资源创建过程中,都要默认经历如下模式转换过程:
用户模式→内核模式→用户模式
从用户模式切换到内核模式是为了创建资源,从内核模式再次切换到用户模式是为了执行应用程序的剩余部分。不仅是资源的创建,与内核对象有关的所有事务都在内核模式下进行。模式切换对系统而言其实也是一种负担,频繁的模式切换会影响性能。
用户模式同步
用户模式同步是用户模式下进行的同步,即无需操作系统的帮助而在应用程序级别进行的同步。用户模式同步的最大优点是——速度快。无需切换到内核模式,仅考虑这一点也比经历内核模式切换的其他方法要快。而且使用方法相对简单,因此,适当运用用户模式同步并无坏处。
但因为这种同步方法不会借助操作系统的力量,其功能上存在一定局限性。稍后将介绍属于用户模式同步的、基于“CRITICAL_SECTION”的同步方法。
内核模式同步
前面已介绍过用户模式同步,即使不另作说明,相信各位也能大概说出内核模式同步的特性及优缺点。下面给出内核模式同步的优点。
□比用户模式同步提供的功能更多。
□可以指定超时,防止产生死锁。
因为都是通过操作系统的帮助完成同步的,所以提供更多功能。特别是在内核模式同步中,可以跨越进程进行线程同步。与此同时,由于无法避免用户模式和内核模式之间的切换,所以性能上会受到一定影响。
大家此时很可能想到:“因为是基于内核对象的操作,所以可以进行不同进程之间的同步!”
因为内核对象并不属于某一进程,而是操作系统拥有并管理的。
基于 CRITICAL_SECTION的同步
基于CRITICAL_SECTION的同步中将创建并运用“CRITICAL_SECTION对象”,但这并非内核对象。与其他同步对象相同,它是进入临界区的一把“钥匙”。因此,为了进入临界区,需要得到CRITICAL_SECTION对象这把“钥匙”。相反,离开时应上交CRITICAL_SECTION对象(以下简称CS)。下面介绍CS对象的初始化及销毁相关函数。
#include<windows.h>
void InitilizerCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
IpCriticalSection //Init...函数中传入需要初始化的CRITICAL_SECTION对象的地址值,Del...函数中传入
//需要解除的CRITICAL_SECTION对象的地址值。
上述函数的参数类型LPCRITICAL_SECTION是CRITICAL_SECTION指针类型。另外DeleteCriticalSection并不是销毁CRITICAL_SECTION对象的函数。该函数的作用是销毁CRITICAL_SECTION对象使用过的(CRITICAL SECTION对象相关的)资源。接下来介绍获取
(拥有)及释放CS对象的函数,可以简单理解为获取和释放“钥匙”的函数。
#include<windows.h>
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
IpCriticalSection //获取(拥有)和释放的CRITICAL_SECTION对象的地址值。
与Linux部分中介绍过的互斥量类似,相信大部分人仅靠这些函数介绍也能写出示例程序。
内核模式的同步方法
典型的内核模式同步方法有基于事件(Event)、信号量、互斥量等内核对象的同步,下面从
互斥量开始逐一介绍。
基于互斥量(Mutual Exclusion)对象的同步
基于互斥量对象的同步方法与基于CS对象的同步方法类似,因此,互斥量对象同样可以理解为“钥匙”。首先介绍创建互斥量对象的函数。
#include<windows.h>
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpName);
//成功时返回创建的互斥量对象句柄,失败时返回NULL。
lpMutexAttributes //传递安全相关的配置信息,使用默认安全设置时可以传递NULL。
blnitialOwner //如果为TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入non-
//signaled状态;如果为FALSE,则创建出的互斥量对象不属于任何线程,此
//时状态为signaled。
IpName //用于命名互斥量对象。传入NULL创建无名的互斥量对象。
从上述参数说明中可以看到,如果互斥量对象不属于任何拥有者,则将进入signaled状态。
利用该特点进行同步。另外,互斥量属于内核对象,所以通过如下函数销毁。
#include<windows.h>
BOOL CloseHandle(HANDLE hObject);//成功时返回TRUE,失败时返回FALSE。
hObject //要销毁对象的句柄
上述函数是销毁内核对象的函数,所同样可以销毁即将介绍的信号量及事件。下面介绍获取和释放互斥量的函数,但我认为只需介绍释放的函数,因为获取是通过各位熟悉的WaitForSingleObject函数完成的。
#include<windows.h>
BOOL ReleaseMutex(HANDLE hMutex);
//成功时返回TRUE,失败时返回FALSE
hMutex //需要释放的互斥量对象句柄
接下来分析获取和释放互斥量的过程。互斥量被某一线程获取时(拥有时)为non-signaled状态,释放时(未拥有时)进入signaled状态。因此,可以使用WaitForSingleObject函数验证互斥量是否已分配。该函数的调用结果有如下2种。
□调用后进入阻塞状态:互压量对象已被其他线程获取,现处于non-signaled状态。
□调用后直接返回:其他线程未占用互斥量对象,现处于signaled状态。
互斥量在WaitForSingleObject函数返回时自动进入non-signaled状态,因为它是第19章介绍过的"auto-reset"模式的内核对象。结果,WaitForSingleObject函数为申请互斥量时调用的函数。因此,基于互斥量的临界区保护代码如下。
WaitForsingleobject(hMutex, INFINITE);
//临界区的开始
//......
//临界区的结束
ReleaseMutex(hMutex);
WaitForSingleObject函数使互斥量进入non-signaled状态,限制访问临界区,所以相当于临界区的门禁系统。相反,ReleaseMutex函数使互斥量重新进入signaled状态,所以相当于临界区的出口。
基于信号量对象的同步
Windows中基于信号量对象的同步也与Linux下的信号量类似,二者都是利用名为“信号量值”的整数值完成同步的,而且该值都不能小于0。当然,Windows的信号量值注册于内核对象。
下面介绍创建信号量对象的函数,当然,其销毁同样是利用CloseHandle函数进行的。
#include <windows.h>
HANDLE Createsemaphore(LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,LONG lInitialCount,
LONG lMaximumCount, LPCTSTR lpName);
//成功时返回创建的信号量对象的句柄,失败时返回NULL。
IpSemaphoreAttributes //安全配置信息采用默认安全设置时传递NULL。
lInitialCount //指定信号量的初始值,应大于0小于lMaximumCount。
IMaximumCount //信号量的最大值。该值为1时,信号量变为只能表示0和1的二进制信号量。
lpName //用于命名信号量对象,传递NULL时创建无名的信号量对象。
可以利用“信量值为0时进入non-signaled状态,大于0时进入signaled状态”的特性进行同步。向lInitialCount参数传递0时,创建non-signaled状态的信号量对象。而向IMaximumCount传入3时,信号量最大值为3,因此可以实现3个线程同时访问临界区时的同步。下面介绍释放信号量对象的函数。
#include <windows.h>
BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG lReleaseCount, LPLONG
lpPreviouscount);
//成功时返回TRUE,失败时返回FALSE。
hSemaphore //传递需要释放的信号量对象.
IReleaseCount //释放意味着信号量值的增加,通过该参数可以指定增加的值。超过最大值则不增加,
//返回FALSE。
IpPreviousCount//用于保存修改之前值的变量地址,不需要时可传递NULL。
信号量对象的值大于0时成为signaled状态,为0时成为non-signaled状态。因此,调用WaitForSingleObject函数时,信号量大于0的情况才会返回。返回的同时将信量值减1,同时进入non-signaled状态(当然,仅限于信号量减1后等于0的情况)。可以通过如下程序结构保护临界区。
WaitForSingleObject(hSemaphore, INFINITE);
//临界区的开始
//..........
//临界区的结束
ReleaseSemaphore(hSemaphore,1, NULL);
基于事件对象的同步
事件同步对象与前2种同步方法相比有很大不同,区别就在于,该方式下创建对象时,在自动以non-signaled状态运行的auto-reset模式和与之相反的manual-reset模式中任选其一。而事件对象的主要特点是可以创建manual-reset模式的对象。首先介绍用于创建事件对象的函数。
#include <windows.h>
HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset,
BOOL bInitialState, LPCTSTR lpName);
//成功时返回创建的事件对象句柄,失败时返回NULL。
IpEventAttributes //安全配置相关参塑,采用默认安全配置时传入NULL。
bManualReset //传入TRUE时创建manual-reset模式的事件对象,传入FALSE时创建auto-
//reset模式的事件对象。
bInitialState //传入TRUE时创建signaled状态的事件对象,传入FALSE时创建non-
//signaled态的事件对象。
IpName //用于命名事件对象。传递NULL时创建无名的事件对象。
相信各位也发现了,上述函数中需要重点关注的是第二个参数。传人TRUE时创建manual-reset模式的事件对象,此时即使WaitForSingleObject函数返回也不会回到non-signaled状态。因此,在这种情况下,需要通过如下2个函数明确更改对象状态。
#include <windows.h>
BOOL ResetEvent(HANDLE hEvent); //设置为non-signaled状态
BOOL SetEvent(HANDLE hEvent); //设置为signaled状态
//成功时返回TRUE,失败时返回FALSE。
传递事件对象句柄并希望改为non-signaled状态时,应调用ResetEvent函数。如果希望改为signaled状态,则可以调用SetEvent函数。
Windows平台下实现多线程服务器端
第18章讲完线程的创建和同步方法后,最终实现了多线程聊天服务器端和客户端。按照这种顺序,本章最后也将在Windows平台下实现聊天服务器端和客户端。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<windows.h>
#include<process.h>
#define BUF_SIZE 100
#define MAX_CLNT 256
unsigned WINAPI HandlerClnt(void *arg);
void SendMsg(char *msg,int len);
void ErrorHandling(char *msg);
int clntCnt=0;
SOCKET clntSocks[MAX_CLNT];
HANDLE hMutex;
int main(int argc,char*argv[]){
WSADATA wasData;
SOCK hServSock,hClntSock;
SOCKADDR_IN servAdr,clntAdr;
int clntAdrSz;
HANDLE hThread;
if(argc!=2){
printf("Usage : %s <port>\n",argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)ErrorHandling("WSAStartup() error!");
hMutex=CreateMutex(NULL,FALSE,NULL);
hServSock=socket(PF_INET,SOCK_STREAM,0);
memset(&servAdr,0,sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=htonl(INADDR_ANY);
servAdr.sin_port=htons(argv[1]);
if(bind(hServSock,(SOCKADDR*)&servAdr,sizeof(servAdr))==SOCK_ERROR);
Error_Handling("bind() error");
if(listen(hServSock,5)==SOCK_ERROR)
Error_Handling("listen() error");
while(1){
clntAdrSz=sizeof(clntAdr);
hClntSock=accept(hServSock,(SOCKADDR*)&clntAdr,&clntAdrsz);
WaitForSingleObject(hMutex,INFINITE);
clntSocks[clntCnt++]=hClntSock;
ReleaseMutex(hMutex);
hThread=(HANDLE)_beginthreadex(NULL,0,HandleClnt,(void*)&hClntSock,0,NULL);
printf("Connected client IP: %s \n",inet_ntoa(clntAdr.sin_addr));
}
closesocket(hServSock);
WSACleanup();
return 0;
}
unsigned WINAPI HandleClnt(void *arg){
SOCKET hClntSock=*((SOCKET*)arg);
int setLen=0,i;
char msg[BUF_SIZE];
while((strLen=recv(hClntSock,msg,sizeof(msg),0))!=0)
SendMsg(msg,strLen);
WaitForSingleObject(hMutex,INFINITE);
for(i=0;i<clntCnt;i++){
if(hClntSock==clntSocks[i]){
while(i++<clntCnt-1)clntSocks[i]=clntSocks[i+1];
break;
}
}
clntCnt--;
ReleaseMutex(hMutex);
closesocket(hClntSock);
return 0;
}
void SendMsg(char *msg,int len){//发送给全部人
int i;
WaitForSingleObject(hMutex,INFINITE);
for(i=0;i<clntCnt;++i)send(clntSocks[i],msg,len,0);
ReleaseMutex(hMutex);
}
void ErrorHangling(char *msg){
fputs(msg,stderr);
fputc('\n',stderr);
exit(1);
}
接下来是聊天客户端。
#include<“头文件声明与之前示例一致,故省略。“>
#define BUF_SIZE 100
#define NAME_SIZE 20
unsigned WINAPI SendMsg(void * arg);
unsigned WINAPI RecvMsg(void * arg);
void ErrorHandling(char * msg);
char name[NAME_SIZE]="[DEFAULT]";
char msg[BUF_SIZE];
int main(int argc, char *argv[]){
WSADATA wsaData;
SOCKET hSock;
SOCKADDR_IN servAdr;
HANDLE hSndThread, hRcvThread;
if(argc!=4){
printf("Usage: %s <IP> <port> <name>\n", argv[0]);
exit(1);
}
if(WSAStartup(MAKEWORD(2,2),&wsaData)!=0)
ErrorHandling("WSAStartup() error!");
sprintf(name,"[%s]", argv[3]);
hSock=socket(PF_INET, SOCK_STREAM,0);
memset(&servAdr, 0, sizeof(servAdr));
servAdr.sin_family=AF_INET;
servAdr.sin_addr.s_addr=inet_addr(argv[1]);
servAdr.sin_port=htons(atoi(argv[2]));
if(connect(hSock, (SOCKADDR*)&servAdr, sizeof(servAdr))==SOCKET_ERROR)
ErrorHandling("connect() error");
hSndThread=(HANDLE)_beginthreadex(NULL,0, Sendmsg,(void*)&hSock,0,NULL);
hRcvThread=(HANDLE)_beginthreadex(NULL,0, RecvMsg,(void*)&hSock,0,NULL);
WaitForSingleObject(hSndThread,INFINITE);
WaitForSingleObject(hRcvThread, INFINITE);
closesocket(hSock);
WSACleanup();
return 0;
}
unsigned WINAPI SendMsg(void * arg){
SOCKET hSock=*((SOCKET*)arg);
char nameMsg[NAME_SIZE+BUF_SIZE];
while(1){
fgets(msg, BUF_SIZE, stdin);
if(!strcmp(msg,"q\n")||!strcmp(msg,"Q\n")){
closesocket(hSock);
exit(0);
}
sprintf(nameMsg,"%s %s",name, msg);
send(hSock,nameMsg,strlen(nameMsg),0);
}
return 0;
}
unsigned WINAPI RecvMsg(void * arg){
int hSock=*((SOCKET*)arg);
char nameMsg[NAME_SIZE+BUF_SIZE];
int strLen;
while(1){
strLen=recv(hSock,nameMsg, NAME_SIZE+BUF_SIZE-1,0);
if(strLen==-1)return -1;
nameMsg[strLen]=0;
fputs(nameMsg, stdout);
}
return 0;
}
void ErrorHandling(char *msg){
//与服务器示例的ErrorHandling一致。
}