第十六章 线程同步与异步套接字编程

来源:互联网 发布:汽车工业的重要性 知乎 编辑:程序博客网 时间:2024/05/18 16:19

事件对象也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。
当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
我们一般不使用人工重置事件对象;
内核对象(包含):事件对象、互斥对象、
API函数HANDLE CreateEvent(LPSECURITY_ATTRIBUTES lpEventAttributes,    //事件对象安全级别,一般设为默认级别,即NULL
     BOOL bManualReset,        //TRUE:人工重置对象;FALSE:自动重置对象;
     BOOL bInitialState,                         //事件对象的初始值,TRUE为“有信号”状态;
     LPTSTR lpName);                             //事件对象名,可以为NULL
创建事件同步对象
非信号状态,则阻塞WaitForSingleObject(g_hEvent,INFINITE)所保护的程序代码;WaitForSingleObject(g_hEvent,INFINITE)函数就是等待同步对象的信号状态的到来;
同步对象为“非信号”状态时,将阻塞WaitForSingleObject()保护的代码。

#include <windows.h>
#include <iostream.h>

DWORD WINAPI Fun1Proc(
  LPVOID lpParameter   // thread data
);

DWORD WINAPI Fun2Proc(
  LPVOID lpParameter   // thread data
);

int tickets=100;
HANDLE g_hEvent;

void main()
{
 HANDLE hThread1;
 HANDLE hThread2;
 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
 hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
 CloseHandle(hThread1);
 CloseHandle(hThread2);

 g_hEvent=CreateEvent(NULL,FALSE,FALSE,NULL);     //自动,初始为“非信号”状态


 g_hEvent=CreateEvent(NULL,FALSE,FALSE,"tickets");  //同样,也可以创建“命名的”事件对象,来实现只启动一个实例
 if(g_hEvent)
 {
  if(ERROR_ALREADY_EXISTS==GetLastError())
  {
   cout<<"only instance can run!"<<endl;
   return;
  }
 }
 SetEvent(g_hEvent);

 Sleep(4000);
 CloseHandle(g_hEvent);
}

DWORD WINAPI Fun1Proc(  LPVOID lpParameter)   // thread data
{
 while(TRUE)
 {
  WaitForSingleObject(g_hEvent,INFINITE);
//  ResetEvent(g_hEvent);                  //不使用人工事件对象的原因:当上句执行完之后,该线程的时间片刚好结束(还没执行ResetEvent(g_hEvent),即事件对象仍处于“信号”状态),则第二个线程可以启动,这样就没有起到让两个线程同步的作用。因此,一般不使用人工事件对象。
  if(tickets>0)
  {
   Sleep(1);
   cout<<"thread1 sell ticket : "<<tickets--<<endl;

  }
  else
     break;
  SetEvent(g_hEvent);           //当自动事件对象时,程序代码执行过WaitForSingleObject(g_hEvent,INFINITE)语句之后,操作系统会自动将该事件对象置为“非信号状态”;
 
 return 0;
}

DWORD WINAPI Fun2Proc(
  LPVOID lpParameter   // thread data
)

 while(TRUE)
 {
  WaitForSingleObject(g_hEvent,INFINITE);
  if(tickets>0)
  {
   Sleep(1);
   cout<<"thread2 sell ticket : "<<tickets--<<endl;   
  }
  else
     break;
  SetEvent(g_hEvent);
 
 return 0;
}

-----------------------------------------------------------------------------------------------

关键代码段(也称临界区)与死锁 

 关键代码段(临界区)工作在用户方式下。
关键代码段(临界区)是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。
函数void InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection)初始化一个临界区对象,其参数用于接受函数回传过来的值,即创建的临界区对象的指针;
函数void EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection )判断临界区对象是否被占有,如果被占有,则阻塞(即不执行,在一直此等,直至....);如果没被占有,则占有之,并继续往下执行;
如果不再需要占有临界区对象,则使用void LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection)函数,释放之;
函数void DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection)释放与指定的没有被占有的临界区对象相关的所有资源;
    哲学家进餐的问题,即“死锁”现象:
线程1拥有了临界区对象A,等待临界区对象B的拥有权,线程2拥有了临界区对象B,等待临界区对象A的拥有权,就造成了死锁。
调用函数Sleep(),则意味着它所在的线程放弃了CPU执行时间;
互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
关键代码段(即临界区对象)是工作在用户方式下,同步速度较快,但在使用关键代码段时,但很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
在MFC应程中使用关键代码段,一般是在类的构造函数中使用InitializeCriticalSection(),在类的析构函数中使用DeleteCriticalSection();

#include <windows.h>
#include <iostream.h>

DWORD WINAPI Fun1Proc(LPVOID lpParameter);   // thread data

DWORD WINAPI Fun2Proc(LPVOID lpParameter);   // thread data

int tickets=100;

CRITICAL_SECTION g_csA;    //定义临界区对象
CRITICAL_SECTION g_csB;

void main()
{
 HANDLE hThread1;
 HANDLE hThread2;
 hThread1=CreateThread(NULL,0,Fun1Proc,NULL,0,NULL);
 hThread2=CreateThread(NULL,0,Fun2Proc,NULL,0,NULL);
 CloseHandle(hThread1);
 CloseHandle(hThread2);

 InitializeCriticalSection(&g_csA);         //创建 电话厅
 //InitializeCriticalSection(&g_csB);   //去掉所有关于g_csB的注释就是死锁
 Sleep(4000);

 DeleteCriticalSection(&g_csA);             //拆除 电话厅
 //DeleteCriticalSection(&g_csB);
}

DWORD WINAPI Fun1Proc(LPVOID lpParameter)   // thread data
{
 while(TRUE)
 {
  EnterCriticalSection(&g_csA);          //判断 电话厅中是否有人;有人,则等;无人,则占有之;
  Sleep(1);                              //上句是试图获取临界区对象的所有权
  //EnterCriticalSection(&g_csB);
  if(tickets>0)
  {
   Sleep(1);
   cout<<"thread1 sell ticket : "<<tickets--<<endl;
  }
  else
   break;
  LeaveCriticalSection(&g_csB);           //离开 电话厅   (注意顺序:此处释放的是B,)
  //LeaveCriticalSection(&g_csA);
 
 return 0;
}

DWORD WINAPI Fun2Proc(LPVOID lpParameter)   // thread data

 while(TRUE)
 {
  //EnterCriticalSection(&g_csB);
  //Sleep(1);
  EnterCriticalSection(&g_csA);

  Sleep(1);
  if(tickets>0)
  {
   Sleep(1);
   cout<<"thread2 sell ticket : "<<tickets--<<endl;
  }
  else
     break;
  LeaveCriticalSection(&g_csA);
  //LeaveCriticalSection(&g_csB);
 }
 //cout<<"thread2 is running!"<<endl;
 return 0;
}

-------------------------------------------------------------------------------------------------

基于消息的异步套接字 

 “异步选择机制”
本应程和上一节的多线程应程不一样,此处是单线程、对网络事件采取基于消息的异步存取策略。
(在同一个线程中,某部分代码只需将字符串送出即可,不必管其他细节,当消息确实已送出且到达目的地,则系统会为字符串到达这个事件(当然必须事先已经注册了)触发一个消息,然后我们可以在该消息相应函数中接收字符串,并作相应处理。)
(如果在同一个线程中,我们仍使用阻塞机制,如果在发送数据之前,程序先执行的是接受数据的语句,则程序会一直等下去,造成阻塞。这是不希望看到的)
    Windows套接字在两种模式下执行I/O操作,阻塞和非阻塞。在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数会一直等待下去,不会立即返回程序(将控制权交还给程序)。而在非阻塞模式下,Winsock函数无论如何都会立即返回。
    Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便地处理网络通信,它对网络事件采用了基于消息的异步存取策略。
    Windows Sockets的异步选择函数WSAAsyncSelect()提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。
前几节课,我们的套接字是工作在阻塞模式下(也即同步模式),因此其执行效率较低;
函数int WSAAsyncSelect(SOCKET s,    //请求事件通知的 套接字
       HWND hWnd,          //接受消息的窗口的句柄
       unsigned int wMsg,  //窗口要接受到的一个消息,它是一个自定义的消息
       long lEvent);   //可通过msdn有关该参数的remarks表格中提供的值的 位或 来构造。
      为一个套接字请求windows基于消息的网络事件通知。可以请求多个事件通知。
可以这样理解:SOCKET m_socket; 定义的套接字是阻塞模式,当调用WSAAsyncSelect()函数注册一个网络事件,也就将相应的套接字(即该函数的一个参数)设置为非阻塞模式。
int WSAEnumProtocols(LPINT lpiProtocols,                      //
        LPWSAPROTOCOL_INFO lpProtocolBuffer,     //
        ILPDWORD lpdwBufferLength);              //
    获取Win32平台所支持的网络协议信息。int WSAEnumProtocols(LPINT lpiProtocols, LPWSAPROTOCOL_INFO lpProtocolBuffer, ILPDWORD lpdwBufferLength );
Win32平台支持多种不同的网络协议,采用Winsock2,就可以编写可直接使用任何一种协议的网络应用程序了。通过WSAEnumProtocols函数可以获得系统中安装的网络协议的相关信息。
lpiProtocols,一个以NULL结尾的协议标识号数组。这个参数是可选的,如果lpiProtocols为NULL,则返回所有可用协议的信息,否则,只返回数组中列出的协议信息。
lpProtocolBuffer,[out],一个用WSAPROTOCOL_INFO结构体填充的缓冲区。 WSAPROTOCOL_INFO结构体用来存放或得到一个指定协议的完整信息。
lpdwBufferLength,[in, out],在输入时,指定传递给WSAEnumProtocols()函数的lpProtocolBuffer缓冲区的长度;在输出时,存有获取所有请求信息需传递给WSAEnumProtocols ()函数的最小缓冲区长度。这个函数不能重复调用,传入的缓冲区必须足够大以便能存放所有的元素。这个规定降低了该函数的复杂度,并且由于一个 机器上装载的协议数目往往是很少的,所以并不会产生问题。
SOCKET类型,其是就是 无符号整形。
语句分析:     m_socket=0;    //这样初始化,是因为SOCKET实际上就是无符号整形;
在对话框类的析构函数中:   closesocket(m_socket);   //关闭套接字,释放为套接字分配的资源;
以前我们创建套节字是在定义的同时创建的,即通过构造函数创建的;这里,我们使用windows的扩展函数来创建。
函数SOCKET WSASocket( int af,                             //地址族
         int type,       //类型
         int protocol,      //协议
      LPWSAPROTOCOL_INFO lpProtocolInfo, //可以选为NULL
      GROUP g,       //
      DWORD dwFlags);      //可取 多值的位或
 创建套接字;以WSA开头的函数都是windows的扩展函数;
SOCKET WSASocket( int af, int type, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags );
前三个参数和socket()函数的前三个参数含义一样。
lpProtocolInfo,一个指向WSAPROTOCOL_INFO结构体的指针,该结构定义了所创建的套接字的特性。如果lpProtocolInfo为NULL,则WinSock2 DLL使用前三个参数来决定使用哪一个服务提供者,它选择能够支持规定的地址族、套接字类型和协议值的第一个传输提供者。如果lpProtocolInfo不为NULL,则套接字绑定到与指定的结构WSAPROTOCOL_INFO相关的提供者。
g,保留的。
dwFlags,套接字属性的描述。
绑定没有提供扩展函数,仍是用bind();
int WSARecvFrom( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, struct sockaddr FAR *lpFrom, LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
s,标识套接字的描述符。
lpBuffers,[in, out],一个指向WSABUF结构体的指针。每一个WSABUF结构体包含一个缓冲区的指针和缓冲区的长度。
dwBufferCount, lpBuffers数组中WSABUF结构体的数目。
lpNumberOfBytesRecvd,[out],如果接收操作立即完成,则为一个指向本次调用所接收的字节数的指针。
lpFlags,[in, out],一个指向标志位的指针。
lpFrom,[out],可选指针,指向重叠操作完成后存放源地址的缓冲区。
lpFromlen,[in, out],指向from缓冲区大小的指针,仅当指定了lpFrom才需要。
lpOverlapped,一个指向WSAOVERLAPPED结构体的指针(对于非重叠套接字则忽略)。
lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。
    When one of the nominated network events occurs on the specified socket s, the application's window hWnd receives message wMsg. The wParam parameter identifies the socket on which a network event has occurred. The low word of lParam specifies the network event that has occurred. The high word of lParam contains any error code. The error code be any error as defined in Winsock2.h.
   int WSASendTo( SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const struct sockaddr FAR *lpTo, int iToLen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
s,标识一个套接字(可能已连接)的描述符。
lpBuffers,一个指向WSABUF结构体的指针。每一个WSABUF结构体包含一个缓冲区的指针和缓冲区的长度。
dwBufferCount, lpBuffers数组中WSABUF结构体的数目。
lpNumberOfBytesSent,[out],如果发送操作立即完成,则为一个指向本次调用所发送的字节数的指针。
dwFlags,指示影响操作行为的标志位。
lpTo,可选指针,指向目标套接字的地址。
iToLen,lpTo中地址的长度。
lpOverlapped,一个指向WSAOVERLAPPED结构的指针(对于非重叠套接字则忽略)。
lpCompletionRoutine,一个指向接收操作完成时调用的完成例程的指针(对于非重叠套接字则忽略)。

 

BOOL CChatApp::InitInstance()
{
 WORD wVersionRequested;
 WSADATA wsaData;
 int err; 
 wVersionRequested = MAKEWORD( 2, 2 ); 
 err = WSAStartup( wVersionRequested, &wsaData );  //因为我们使用的是winsock2.2版本,所以不能像上一节课那样,调用函数AfxSocketInit()来加载套接字库,因为这个函数加载的是1.1版本的,此处不满足要求。
 if ( err != 0 ) {                           //在上面的析构函数中,终止对该套接字库的使用。
  return FALSE;
 
 if ( LOBYTE( wsaData.wVersion ) != 2 ||
        HIBYTE( wsaData.wVersion ) != 2 ) {
  WSACleanup( );
  return FALSE;
 }

    ...

 

BOOL CChatDlg::InitSocket()
{
 m_socket=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,0);
 if(INVALID_SOCKET==m_socket)
 {
  MessageBox("创建套接字失败!");
  return FALSE;
 }
 SOCKADDR_IN addrSock;
 addrSock.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
 addrSock.sin_family=AF_INET;
 addrSock.sin_port=htons(8000);
 if(SOCKET_ERROR==bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR)))
 {
  MessageBox("绑定失败!");
  return FALSE;
 }
 if(SOCKET_ERROR==WSAAsyncSelect(m_socket,m_hWnd,UM_SOCK,FD_READ))    //注册网络事件;请求的网络事件由最后一个参数指定,其可供取值的范围可参照msdn;
 {
  MessageBox("注册网络读取事件失败!");
  return FALSE;
 }

 return TRUE;
}

 

BEGIN_MESSAGE_MAP(CChatDlg, CDialog)   //CChatDlg.cpp中的消息映射
 //{{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()

 

void CChatDlg::OnSock(WPARAM wParam,LPARAM lParam)
{
 switch(LOWORD(lParam))   //此处其实是不需要作switch逐个判断的,因为我们就注册了一个网络通知事件,但要有良好的代码风格,就应该如此,因为我们完全可以注册多个网络通知事件。
 {
 case FD_READ:
  WSABUF wsabuf;
  wsabuf.buf=new char[200];
  wsabuf.len=200;
  DWORD dwRead;                 //实际接受的字符数
  DWORD dwFlag=0;               //因要有回传值,所以定义该变量
  SOCKADDR_IN addrFrom;
  int len=sizeof(SOCKADDR);
  CString str;
  CString strTemp;
  HOSTENT *pHost;
  if(SOCKET_ERROR==WSARecvFrom(m_socket,&wsabuf,1,&dwRead,&dwFlag,
      (SOCKADDR*)&addrFrom,&len,NULL,NULL))   //本函数是在FD_READ分支下调用的,一般不会调用失败。
  {
   MessageBox("接收数据失败!");
   return;
  }
  pHost=gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr,4,AF_INET);
  //str.Format("%s说 :%s",inet_ntoa(addrFrom.sin_addr),wsabuf.buf);  
  str.Format("%s说 :%s",pHost->h_name,wsabuf.buf);
  str+="\r\n";
  GetDlgItemText(IDC_EDIT_RECV,strTemp);
  str+=strTemp;
  SetDlgItemText(IDC_EDIT_RECV,str);
  break;
 }
}

void CChatDlg::OnBtnSend()
{
 // TODO: Add your control notification handler code here
 DWORD dwIP;           //IP控件的GetAddress(dwIP)成员函数,回传的IP地址就是DWORD类型;
 CString strSend;
 WSABUF wsabuf;
 DWORD dwSend;             //实际发送的字节数目
 int len;
 CString strHostName;
 SOCKADDR_IN addrTo;
 HOSTENT* pHost;
 if(GetDlgItemText(IDC_EDIT_HOSTNAME,strHostName),strHostName=="")     //如果没有输入主机名,我们通过IP控件得到IP地址
 {
  ((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
  addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
 }
 else
 {
  pHost=gethostbyname(strHostName);
  addrTo.sin_addr.S_un.S_addr=*((DWORD*)pHost->h_addr_list[0]);   //矛盾:addrTo.sin_addr.S_un.S_addr 需要的是以网络字节顺序表示的u_long类型变量;而 pHost->h_addr_list[0] 是以网络字节顺序表示的char*类型变量;u_long类型 就是DWORD 类型;
 }
 
 addrTo.sin_family=AF_INET;
 addrTo.sin_port=htons(8000);

 GetDlgItemText(IDC_EDIT_SEND,strSend);
 len=strSend.GetLength();
 wsabuf.buf=strSend.GetBuffer(len);    //GetBuffer()将一个CString对象转换成char*返回。
 wsabuf.len=len+1;

 SetDlgItemText(IDC_EDIT_SEND,"");    //清空

 if(SOCKET_ERROR==WSASendTo(m_socket,&wsabuf,1,&dwSend,0,   //第三个参数指定第二个参数指向的数据中包含WSABUF结构体的个数。
   (SOCKADDR*)&addrTo,sizeof(SOCKADDR),NULL,NULL))
 {
  MessageBox("发送数据失败!");
  return;
 }
}

 

终止套接字库的使用

CChatApp::~CChatApp()
{
 // TODO: add construction code here,
 // Place all significant initialization in InitInstance
 WSACleanup();                    //终止套接字库的使用
}

 

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

}


本文出处:http://blog.sina.com.cn/s/blog_690e57390100l3ol.html

0 0
原创粉丝点击