TCP/IP网络编程 第二十章:Windows中的线程同步

同步方法的分类及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
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
void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection);
IpCriticalSection   //获取(拥有)和释放的CRITICAL_SECTION对象的地址值。

与Linux部分中介绍过的互斥量类似,相信大部分人仅靠这些函数介绍也能写出示例程序。

内核模式的同步方法

典型的内核模式同步方法有基于事件(Event)、信号量、互斥量等内核对象的同步,下面从
互斥量开始逐一介绍。

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

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

#include
HANDLE CreateMutex(LPSECURITY_ATTRIBUTES lpMutexAttributes,BOOL bInitialOwner,LPCTSTR lpName);
//成功时返回创建的互斥量对象句柄,失败时返回NULL。
       lpMutexAttributes    //传递安全相关的配置信息,使用默认安全设置时可以传递NULL。
       blnitialOwner        //如果为TRUE,则创建出的互斥量对象属于调用该函数的线程,同时进入non- 
                            //signaled状态;如果为FALSE,则创建出的互斥量对象不属于任何线程,此 
                            //时状态为signaled。
       IpName               //用于命名互斥量对象。传入NULL创建无名的互斥量对象。     

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

#include
BOOL CloseHandle(HANDLE hObject);//成功时返回TRUE,失败时返回FALSE。
     hObject    //要销毁对象的句柄

上述函数是销毁内核对象的函数,所同样可以销毁即将介绍的信号量及事件。下面介绍获取和释放互斥量的函数,但我认为只需介绍释放的函数,因为获取是通过各位熟悉的WaitForSingleObject函数完成的。

#include
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 
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 
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 
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 
BOOL ResetEvent(HANDLE hEvent); //设置为non-signaled状态
BOOL SetEvent(HANDLE hEvent);   //设置为signaled状态
//成功时返回TRUE,失败时返回FALSE。
     

传递事件对象句柄并希望改为non-signaled状态时,应调用ResetEvent函数。如果希望改为signaled状态,则可以调用SetEvent函数。

 Windows平台下实现多线程服务器端

第18章讲完线程的创建和同步方法后,最终实现了多线程聊天服务器端和客户端。按照这种顺序,本章最后也将在Windows平台下实现聊天服务器端和客户端。

#include
#include
#include
#include
#include

#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 \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

接下来是聊天客户端。

#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   \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一致。
}

你可能感兴趣的:(TCP/IP网络编程,windows,服务器,tcp/ip,网络协议,网络)