第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 += "\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);
}
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);