Lesson 16 线程同步与异步套接字

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.       互斥对象,事件对象,关键代码段的比较

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

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

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_DGRAMUDP

 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加载系统安装可用版本,CChatAppinitInstance函数加入

//加载套接字库和进行版本的协商

         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响应函数原型声明

protected:

HICON m_hIcon;

// Generated message map functions

//{{AFX_MSG(CChatDlg)

virtual BOOL OnInitDialog();

afx_msg void OnSysCommand(UINT nID, LPARAM lParam);

afx_msg void OnPaint();

afx_msg HCURSOR OnQueryDragIcon();

afx_msg void OnBtnSend();

//}}AFX_MSG

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

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

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

DECLARE_MESSAGE_MAP()

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

BEGIN_MESSAGE_MAP(CChatDlg, CDialog)

//{{AFX_MSG_MAP(CChatDlg)

ON_WM_SYSCOMMAND()

ON_WM_PAINT()

ON_WM_QUERYDRAGICON()

ON_BN_CLICKED(IDC_BTN_SEND, OnBtnSend)

//}}AFX_MSG_MAP

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

END_MESSAGE_MAP()

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

最好对所接受的消息进行判断后处理,本例中只有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 += "\r\n";

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

}

 

4.    利用主机名实现网络访问

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