第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);

 

原文地址:https://www.cnblogs.com/happykoukou/p/9379167.html