zoukankan      html  css  js  c++  java
  • 第16章 线程同步与异步套接字

    转自: https://blog.csdn.net/u014162133/article/details/46573873

    1、事件对象

    事件对象同上一课中的互斥对象一样属于内核对象,它包含三个成员:使用读数,用于指明该事件是一个自动重置的还是人工重置的事件的布尔值,用于指明该事件处于已通知状态还是未通知状态的布尔值.

    当人工重置的事件对象得到通知时,等待该事件对象的所有线程都变为可调度线程,而一个自动重置的事件对象得到通知时,等待该事件对象的线程中人有一个变为可调度线程.所以一般使用线程同步时使用自动重置.

    创建事件对象:

    HANDLE CreateEvent(

      LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全选项,默认为NULL

      BOOL bManualReset,                       // reset type,TRUE(人工),FALSE(自动)

      BOOL bInitialState,                      // initial state,TRUE(有信号状态)

      LPCTSTR lpName                           // object name.事件对象名

    );

    BOOL SetEvent(HANDLE hEvent);把指定的事件对象设置为有信号状态

    BOOL ReSetEvent(HANDLE hEvent);把指定的事件对象设置为无信号状态

    BOOL CloseHandle(  HANDLE hObject );  // handle to object关闭事件对象

    DWORD WaitForSingleObject(//请求内核对象,一旦得到事件对象,就进入代码中

      HANDLE hHandle,        // handle to object

      DWORD dwMilliseconds   // time-out interval

    );

    以下是一个模拟火车站售票的多线程程序(使用事件对象实现线程同步)

    #include <windows.h>//加入头文件,Window API库
    #include <iostream.h>//C++标准输入输出库
    
    int tickets = 100;//共享的资源,火车票
    HANDLE g_hEvent;//全局的事件对象句柄
    //线程处理函数原型声明
    DWORD WINAPI Thread1Proc(
        LPVOID lpParameter   // thread data
    );
    DWORD WINAPI Thread2Proc(
        LPVOID lpParameter   // thread data
    );
    
    void main(){
    //     g_hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
        //创建一个人工重置的匿名事件对象,当调用SetEvent时所有的线程都可以执行,不能实现同步
    //     SetEvent(g_hEvent);//将事件对象设置为有信号状态
        g_hEvent = CreateEvent(NULL, FALSE, FALSE, "tickets");
        //创建一个自动重置的有名事件对象,当调用SetEvent时只有一个线程可以执行
        SetEvent(g_hEvent);
        //可以通过创建有名的事件对象来实现只有一个程序实例运行
        if (g_hEvent)//有值
        {
            if (ERROR_ALREADY_EXISTS == GetLastError())//以事件对象存在为条件实现只有一个实例运行限制,因为事件对象是内核对象,由操作系统管理,因此可以在多个线程间访问
            {
                cout << "only one instance can run!" << endl;
                return;
            }
        }
        HANDLE hThread1;
        HANDLE hThread2;
        hThread1 = CreateThread(NULL, 0, Thread1Proc, NULL, 0, NULL);
        hThread2 = CreateThread(NULL, 0, Thread2Proc, NULL, 0, NULL);
        CloseHandle(hThread1);//释放线程句柄
        CloseHandle(hThread2);
    
        Sleep(4000);
        CloseHandle(g_hEvent);//注意最后释放事件对象句柄,在MFC中在类的析构函数中完成
    }
    
    DWORD WINAPI Thread1Proc(
        LPVOID lpParameter   // thread data
    )
    {
        //其中的SetEvent函数应该在两个判断中都调用,以防止因条件不满足而造成对象不能被设置为有信息状态
        while(TRUE){
            WaitForSingleObject(g_hEvent, INFINITE);//无限期等待事件对象为有信号状态
            if (tickets > 0)//进入保护代码
            {
                cout << "Thread1 is selling tickets : " << tickets-- << endl;
                SetEvent(g_hEvent);
            }
            Else//如果票已经售完,退出循环
            {
                break;
                SetEvent(g_hEvent);
            }
        }
        return 0;
    }
    DWORD WINAPI Thread2Proc(
        LPVOID lpParameter   // thread data
    )
    {
        while(TRUE){
            WaitForSingleObject(g_hEvent, INFINITE);
            //等待事件对象,如果对象为有信号状态,可以请求该对象资源,并将其设置为无信息状态 
            if (tickets > 0)
            {
                cout << "Thread2 is selling tickets : " << tickets-- << endl;
                SetEvent(g_hEvent);
            }
            else
            {
                break;
                SetEvent(g_hEvent);//设置事件对象为有信号状态
            }
        }
        return 0;
    }

    综上:为实现线程间的同步,不应该使用人工重置的事件对象,而应该使用自动重置的事件对象

    2、关键代码段(临界区)

    工作在用户方式下,它是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权,通常把多线程访问同一种资源的那部分代码当作关键代码段.

    VOID InitializeCriticalSection(//初始化代码段

      LPCRITICAL_SECTION lpCriticalSection  //[out] critical section,使用之前要构造

    );

    VOID EnterCriticalSection(//进入关键代码段(临界区)

      LPCRITICAL_SECTION lpCriticalSection  // critical section

    );

    VOID LeaveCriticalSection(//离开关键代码段(临界区)

      LPCRITICAL_SECTION lpCriticalSection   // critical section

    );

    VOID DeleteCriticalSection(//删除关键代码段(临界区)

      LPCRITICAL_SECTION lpCriticalSection   // critical section

    );

    这种方法比较简单!但缺点是如果使用多了关键代码段,容易造成线程的死锁(使用两个或以上的临界区对象或互斥对象,造成线程1拥有了临界区对象A,等待临界区对象B的拥有权,线程2拥有了临界区对象B,等待临界区对象A的拥有权,形成死锁,程序无法执行下去!

    3、互斥对象,事件对象,关键代码段的比较

    (1) 互斥对象和事件对象都属于内核对象,利用内核对象进行线程同步时,较慢,但利用互斥对象和事件对象这种内核对象,可以在多个进程中的各个线程间进行同步

    (2) 关键代码段工作在用户方式下,同步速度快,但很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值

    4、基于消息的异步套接字编程

    Windows套接字在两种模式下执行I/O操作:阻塞模式和非阻塞模式.

    在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数会一直等待下去,不会立即返回(也就是不地将控制权交还给程序),例如,程序中调用了recvfrom函数后,如果这时网络上没有数据传送过来,该函数就会阻塞程序的执行,从而导致调用线程暂停运行,但不会阻塞主线程运行.

    在非阻塞模式下,Winsock函数无论如何都会立即返回,在该函数执行的操作完成之后,系统会采用某种方式将操作结果通知给调用线程,后者根据通知信息可以判断该操作是否正常完成.

    Windows Sockets采用了基于消息的异步存取策略以支持Windows的消息驱动机制,Windows Sockets的异步选择函数WSAAsyncSelect提供了消息机制的网络事件选择,当使用它登录的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,指示发生的网络事件,以及与该事件相关的一些信息.因此可针对不同的网络事件进行登录,一旦有数据到来,就会触发这个事件,操作系统就会通过一个消息来通知调用线程,后者就可以在相应的消息响应函数中接收这个数据.因为是在该数据到来之后,操作系统发出的通知,所以这时肯定能够接收这个数据.异步套接字能够有效的提高应用程序的性能.

    一些主要函数

    (1) 为指定的套接字请求基于Windows消息的网络事件通知.自动设置为非阻塞模式

    int WSAAsyncSelect(

      SOCKET s,           //标识请求网络事件通知的套接字描述符

      HWND hWnd,        //标识一个网络事件发生时接收消息的窗口的句柄

      unsigned int wMsg,     //指定网络事件发生时窗口将接收到的消息,(自定义消息)

      long lEvent           //指定网络事件类型,可以位或操作组合使用

    );

    (2) 获得系统中安装的网络协议的相关信息

    int WSAEnumProtocols(

      LPINT lpiProtocols,//[in]以NULL结尾的协议标识号数组.如果为NULL,返回可用信息

      LPWSAPROTOCOL_INFO lpProtocolBuffer,//[out]存放指定的完整信息

      ILPDWORD lpdwBufferLength//[in,out]输入时传递缓冲区长度,输出最小缓冲区长度

    );

    (3) 初始化进程使用的WS2_32.DLL

    int WSAStartup(

      WORD wVersionRequested,//高位字节指定Winsock库的副版本,低位字节是主版本号

      LPWSADATA lpWSAData//[out]用来接收Windows Sockets实现细节

    );

    (4) 终止对套字库WS2_32.DLL的使用

    int  WSACleanup (void);

    (5) Winsock库中的扩展函数WSASocket将创建套接字

    SOCKET WSASocket(

      int af,//地址簇标识

      int type,//socket类型SOCK_DGRAM为UDP

      int protocol,//协议簇

      LPWSAPROTOCOL_INFO lpProtocolInfo,//定义创建套接字的特性,如果为NULL,则

                                 //WinSock2.Dll使用前三个参数决定使用哪个服务提供者

      GROUP g,//保留

      DWORD dwFlags//指定套接字属性的描述,如果为WSA_FAG_OVERLAPPED则为一个重叠套接字,与文件中相似,

    );

    然后在套接字上调用WSASend, WSARecv,WSASendTo,WSARecvFrom,SWAIoctl这些函数都会立即返回,这些操作完成后,操作系统会通过某种方式来通知调用线程,后者就可以根据通知信息判断操作是否完成

    (6) WSARecvFrom接收数据报类型的数据,并保存数据发送方的地址

    int WSARecvFrom(

      SOCKET s,//套接字描述符

      LPWSABUF lpBuffers,//指向WSABUF数据指针,一个成员缓冲区指针buf,另个长度

      DWORD dwBufferCount,//lpBuffers数组中WSABUF结构体的数上,一般为1

      LPDWORD lpNumberOfBytesRecvd,//[out]接收完成后数据字节数指针

      LPDWORD lpFlags,//[in/out]标志会影响函数行为,设置为0即可

      struct sockaddr FAR *lpFrom,//[out]可选,指向重叠操作完成后存放源地址的缓冲区

      LPINT lpFromlen,//[in/out]指定lpFrom缓冲区大小的指针

      LPWSAOVERLAPPED lpOverlapped,//指向重叠套接字指针,非重叠忽略

      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine//一个指定接收完成时调用的完成全程指针(非重叠套接字的忽略0

    );

    如果创建是重叠套接字,最后两个参数值要设置,因为这时将会采用重叠I/O,函数会返回,当接收数据这一操作完成后,操作系统会调用lpCompletionRoutine参数指定的例程来通知调用线程,这个例程就是一个回调函数.

    (7) WSASendTo发送数据报类型的数据

    int WSASendTo(

      SOCKET s,//套接字描述符

      LPWSABUF lpBuffers,

      DWORD dwBufferCount,

      LPDWORD lpNumberOfBytesSent,

      DWORD dwFlags,//0即可

      const struct sockaddr FAR *lpTo,//可选指针,指向目标套接字的地址

      int iToLen,//lpTo中地址长度

      LPWSAOVERLAPPED lpOverlapped,

      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine

    );

    5、一个网络聊天室程序的实现

    新建工程基于对话框,工程名为Chat,并添加一些控件主要两个编辑,IP控件和发送按钮

    (1) 加载套接字库

    需要加载套接字库并进行版本协商,AfxSocketInit只能加载1.1版本的套接字库,本例使用WSAStartup加载系统安装可用版本,在CChatApp的initInstance函数加入

    //加载套接字库和进行版本的协商
        WORD wVersionRequested;
        WSADATA wsaData;
        int err;
        
        wVersionRequested = MAKEWORD( 2, 2 );//2.2版本
        
        err = WSAStartup( wVersionRequested, &wsaData );
        if ( err != 0 ) {                                
            return FALSE;
        }    
        if ( LOBYTE( wsaData.wVersion ) != 2 ||HIBYTE( wsaData.wVersion ) != 2 ) {
            WSACleanup( );
            return FALSE; 
        }

    并在stdafx.h文件中加入头文件 #include <winsock2.h>

    (2) 创建并初始化套接字

    在CChatDlg类增加一个SOCKET类型的成员变量,m_socket,设为私有,再添加一个BOOL类型的成员函数:InitSocket,初始化该类的套接字成员

    BOOL CChatDlg::InitSocket()
    {
        //使用扩展函数创建套接字
        m_socket = WSASocket(AF_INET, SOCK_DGRAM, 0, NULL, NULL, 0);
        if (INVALID_SOCKET == m_socket)
        {
            MessageBox("创建套接字失败!");
            return FALSE;
        }
        //要绑定套按字的本地址和协议簇,端口号
        SOCKADDR_IN addrSock;
        addrSock.sin_addr.S_un.S_addr = htonl(ADDR_ANY);
        addrSock.sin_family = AF_INET;
        addrSock.sin_port = htons(6000);
        //绑定套接字到本地套按地址上
        if(SOCKET_ERROR == bind(m_socket, (SOCKADDR*)&addrSock, sizeof(SOCKADDR))){
            MessageBox("绑定失败!");
            return FALSE;
        }
    
        //调用WSAAsyncSelect(m_socket,m_hWnd,UM_SOCK,FD_READ)为网络事件定义消息!
        //此时如果发生FD_READ网络事件,系统会发送UM_SOCK(自定义)消息给应用程序!
        //使用相应的消息响应函数来处理,程序并不会阻塞在这儿了!
        if (SOCKET_ERROR == WSAAsyncSelect(m_socket, m_hWnd, UM_SOCK, FD_READ))
        {
            MessageBox("创建网络事件消息处理失败!");
            return FALSE;
        }
        //剩下的就是在相应的UM_SOCK消息中进行处理了,注意的是:定义的消息要带参数,LPARAM中的低字节是保存网络事件(如FD_READ),
        //高字节保存错误信息,WPARAM保存是发生网络事件的SOCKET标识
        return TRUE;
    }

    CChatDlg类的OnInitDialog函数中调用这个函数,完成套接字的初始化工作

    (3) 实现接收端的功能

    在CChatDlg头文件中定义自定义的消息:UM_SOCK

    #define UM_SOCK WM_USER + 1

    在CChatDlg头文件中添加UM_SOCK响应函数原型声明

    //定义的消息要带参数,LPARAM中的低字节是保存网络事件(如FD_READ),

    //高字节保存错误信息,WPARAM保存是发生网络事件的SOCKET标识

    afx_msg void OnSock(WPARAM, LPARAM);//自定义消息的响应函数原型

    在CChatDlg类的源文件中添加UM_SOCK消息映射

    ON_MESSAGE(UM_SOCK, OnSock)//消息与其响应函数的映射

                  消息响应函数的实现,因为同时可以请求多个网络事件如FD_READ或RDWRITE

    最好对所接受的消息进行判断后处理,本例中只有FD_READ,但仍判断处理,要注意是消息接收两个参数,低字节是保存网络事件(如FD_READ),高字节保存错误信息,WPARAM保存是发生网络事件的SOCKET标识.

    //自定义消息响应函数的定义
    void CChatDlg::OnSock(WPARAM wParam, LPARAM lParam){
        switch (LOBYTE(lParam))
        {
        case FD_READ://网络读取事件
            WSABUF wsaBuf;
            char recvBuf[200];
            wsaBuf.buf = recvBuf;
            wsaBuf.len = 200;
            DWORD dwRead;
            DWORD dwFlag = 0;
    
            SOCKADDR_IN addrFrom;
            int len = sizeof(SOCKADDR);
            if(SOCKET_ERROR == WSARecvFrom(m_socket, &wsaBuf, 1, &dwRead, &dwFlag, (SOCKADDR*)&addrFrom, &len, NULL, NULL)){
                MessageBox("接收网络数据失败!");
                return;
            }
            CString strRecv;
            CString strTemp;
            strRecv.Format("%s 说: %s", inet_ntoa(addrFrom.sin_addr), recvBuf);
            GetDlgItemText(IDC_EDIT_RECV, strTemp);
            strRecv += "
    ";
            strRecv += strTemp;
            SetDlgItemText(IDC_EDIT_RECV, str);
            break;
        }
    }

    (4) 发送端按钮的实现

    void CChatDlg::OnBtnSend() 
    {
        // TODO: Add your control notification handler code here
        DWORD ip;    
        WSABUF wsaBuf;    
        SOCKADDR_IN addrTo;
        CString strSend;
        int len;
        DWORD dwSend;
        ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(ip);
        
        addrTo.sin_addr.S_un.S_addr = htonl(ip);
        addrTo.sin_family = AF_INET;
        addrTo.sin_port = htons(6000);
        
        GetDlgItemText(IDC_EDIT_SEND, strSend);    
        len = strSend.GetLength();
        wsaBuf.buf = strSend.GetBuffer(len);
        wsaBuf.len = len + 1;    
        SetDlgItemText(IDC_EDIT_SEND, "");
        //发送数据
        if(SOCKET_ERROR==WSASendTo(m_socket,&wsaBuf,1,&dwSend,0,
            (SOCKADDR*)&addrTo,sizeof(SOCKADDR),NULL,NULL))
        {
            MessageBox("发送数据失败!");
            return;
        }    
    }

    (5) 终止套接字库的使用

    为CChatApp类增加一个析构函数,主要是在此函数中调用WSACleanup函数,终止对套接字库的使用

    CChatApp::~CChatApp()
    {
        WSACleanup();//释放套接字
    }

    (6) 在CChatDlg类中关闭套接字,添加一个析构函数,首先判断是否该套接字库有值,如果有的话关闭套接字

    CChatDlg::~CChatDlg(){
        closesocket(m_socket);
    }

    6、利用主机名实现网络访问

           struct hostent FAR *gethostbyname(

                 const char FAR *name  //从主机名中获取IP地址

    );

    Hostent结构体:

    struct hostent {

      char FAR *       h_name;

      char FAR * FAR * h_aliases;

      short            h_addrtype;

      short            h_length;

      char FAR * FAR * h_addr_list;//空中止的IP地址列表,是一个char*字符数组,因为一个

                                     //主机可能有多个IP,选择第一个即可

    };

    由主机IP转换成主机名

    struct HOSTENT FAR * gethostbyaddr(

      const char FAR *addr,//指向网络字节序表示的IP地址指针

      int len,//地址长度,对于AF_INET必须为4

      int type//类型AF_INET

    );

    接收方部分代码可改为;

    HOSTENT *pHost;
    pHost = gethostbyadd((char*)&addrFrom.sin_addr.S_un.S_addr, 4, AF_INET);
    str.Format(“%s说:%s”, pHost->h_name, wsabuf.buf);

     

  • 相关阅读:
    jsp第七次作业
    jsp第二次作业
    第四次JSP作业
    软件测试练习第一次
    JSP第一次课后作业
    读信息
    购物商城
    页面跳转
    安卓第7周作业
    安卓第六周作业
  • 原文地址:https://www.cnblogs.com/happykoukou/p/9379167.html
Copyright © 2011-2022 走看看