您的位置:首页 > 编程语言

孙鑫VC学习(第16课:线程同步与异步套接字编程)

2011-05-23 20:00 387 查看
孙鑫16课:线程同步与异步套接字编程
利用事件对象实现线程同步:
事件对象(互斥对象也属于内核对象)也属于内核对象,包含一个使用计数,一个用于指明该事件是一个自动重置的事件还是一个人工重置的事件的布尔值,另一个用于指明该事件处于已通知状态还是未通知状态的布尔值。
有两种不同类型的事件对象。一种是人工重置的事件,另一种是自动重置的事件。当人工重置的事件得到通知时,等待该事件的所有线程均变为可调度线程。当一个自动重置的事件得到通知时,等待该事件的线程中只有一个线程变为可调度线程。
建立一个WIN32的控制台应用程序:
#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);

if(tickets>0)

{

Sleep(1);

cout<<"thread1 sell ticket : "<<tickets--<<endl;

}

else

break;

SetEvent(g_hEvent);

}

return 0;
}

DWORD WINAPI Fun2Proc(

LPVOID lpParameter // thread data
)
{

while(TRUE)

{

WaitForSingleObject(g_hEvent,INFINITE);
//
ResetEvent(g_hEvent);

if(tickets>0)

{

Sleep(1);

cout<<"thread2 sell ticket : "<<tickets--<<endl;

}

else

break;

SetEvent(g_hEvent);

}

return 0;
}
下面创建另一个线程同步的方式:关键代码段:
关键代码段(临界区)工作在用户方式下。
关键代码段(临界区)是指一个小代码段,在代码能够执行前,它必须独占对某些资源的访问权。
可以把访问同一种资源的代码看成是关键代码段。
新建一个WIN32的控制台程序:
#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 CriticalSection;

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(&CriticalSection);

Sleep(4000);

DeleteCriticalSection(&CriticalSection);
}

DWORD WINAPI Fun1Proc(

LPVOID lpParameter // thread data

)
{

while(TRUE)

{

EnterCriticalSection(&CriticalSection); //在要保护的资源代码前加上这句,

//在访问后释放临界区对象所有权。如果得不到对临界资源的访问权,则线程等待下去。

//线程1执行完成之后,线程1退出,但是如果线程1没有释放临界区对象的所有权,则线程2一直等待临界区对象的使用权,

//则线程2无法得到临界区对象的使用权,线程2一直等,直到主线程退出。进程退出,线程2也退出。

if(tickets>0)

{

Sleep(1);

cout<<"thread1 sell ticket : "<<tickets--<<endl;

}

else

break;

LeaveCriticalSection(&CriticalSection);

}

return 0;
}

DWORD WINAPI Fun2Proc(

LPVOID lpParameter // thread data

)
{

while(TRUE)

{

EnterCriticalSection(&CriticalSection);

if(tickets>0)

{

Sleep(1);

cout<<"thread2 sell ticket : "<<tickets--<<endl;

}

else

break;

LeaveCriticalSection(&CriticalSection);

}

return 0;
}
在使用临界区对象的时候,要注意释放临界区对象的所有权。
要注意死锁的问题。死锁问题:线程1拥有了临界区对象A,等待临界区对象B的拥有权,线程2拥有了临界区对象B,等待临界区对象A的拥有权,就造成了死锁。死锁代码如下:
#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);

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

LeaveCriticalSection(&g_csA);

}

return 0;
}

DWORD WINAPI Fun2Proc(

LPVOID lpParameter // thread data
)
{

while(TRUE)

{

EnterCriticalSection(&g_csB);

Sleep(1);

EnterCriticalSection(&g_csA);

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

三种实现线程同步的方式的比较:
互斥对象、事件对象与关键代码段的比较
n
互斥对象和事件对象属于内核对象,利用内核对象进行线程同步,速度较慢,但利用互斥对象和事件对象这样的内核对象,可以在多个进程中的各个线程间进行同步。
n
关键代码段是工作在用户方式下,同步速度较快,但在使用关键代码段时,很容易进入死锁状态,因为在等待进入关键代码段时无法设定超时值。
推荐书目
《 Windows核心编程》
机械工业出版社
在实现线程同步时,首选关键代码段。若在MFC程序中,可以在一个类的构造函数中调用InitializeCriticalSection();在析构函数中调用DeleteCriticalSection()。在要保护的代码前面加上EnterCriticalSection();在访问完要保护的资源后调用LeaveCriticalSection();记得一定要释放关键代码段。如果构造了多个临界区对象,要注意线程死锁。多个进程的各个线程间,要用互斥对象和事件对象。

基于消息的异步套接字编程:
n
Windows套接字在两种模式下执行I/O操作,阻塞和非阻塞。在阻塞模式下,在I/O操作完成前,执行操作的Winsock函数会一直等待下去,不会立即返回程序(将控制权交还给程序)。而在非阻塞模式下,Winsock函数无论如何都会立即返回。
n
Windows Sockets为了支持Windows消息驱动机制,使应用程序开发者能够方便地处理网络通信,它对网络事件采用了基于消息的异步存取策略。
n
Windows Sockets的异步选择函数WSAAsyncSelect()提供了消息机制的网络事件选择,当使用它登记的网络事件发生时,Windows应用程序相应的窗口函数将收到一个消息,消息中指示了发生的网络事件,以及与事件相关的一些信息。

int WSAEnumProtocols( LPINT
lpiProtocols,
LPWSAPROTOCOL_INFO lpProtocolBuffer, ILPDWORD
lpdwBufferLength );
n
Win32平台支持多种不同的网络协议,采用Winsock2,就可以编写可直接使用任何一种协议的网络应用程序了。通过WSAEnumProtocols函数可以获得系统中安装的网络协议的相关信息。
n
lpiProtocols,一个以NULL结尾的协议标识号数组。这个参数是可选的,如果lpiProtocols为NULL,则返回所有可用协议的信息,否则,只返回数组中列出的协议信息。
n
lpProtocolBuffer,[out],一个用WSAPROTOCOL_INFO结构体填充的缓冲区。
WSAPROTOCOL_INFO结构体用来存放或得到一个指定协议的完整信息。
n
lpdwBufferLength,[in, out],在输入时,指定传递给WSAEnumProtocols()函数的lpProtocolBuffer缓冲区的长度;在输出时,存有获取所有请求信息需传递给WSAEnumProtocols
()函数的最小缓冲区长度。这个函数不能重复调用,传入的缓冲区必须足够大以便能存放所有的元素。这个规定降低了该函数的复杂度,并且由于一个
机器上装载的协议数目往往是很少的,所以并不会产生问题。
下面采用异步套接字编写一个网络聊天室程序:
新建一个基于对话框的程序:
在BOOL CChatApp::InitInstance()中加入(在APP里添加的位置也是有讲究的):
WORD wVersionRequested;

WSADATA wsaData;

int err;

wVersionRequested = MAKEWORD( 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>
再LINK
Ws2_32.lib
给CChatApp增加一个析构函数,在此函数中终止对套接字库的使用:
CChatApp::~CChatApp()
{

WSACleanup( );
}
为CChatDlg增加private:

SOCKET m_socket;
在CChatDlg::CChatDlg中对套接字进行初始化。m_socket=0;
在析构函数中关闭套接字:
CChatDlg::~CChatDlg()
{

if (m_socket)

{

closesocket(m_socket);

}
}
在CChatDlg中增加BOOL
CChatDlg::InitSocket()函数。函数代码如下:
BOOL CChatDlg::InitSocket()
{

m_socket=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,0);

if (INVALID_SOCKET ==m_socket)

{

MessageBox("创建套接字失败!");

return FALSE;

}

SOCKADDR_IN skaddr;

skaddr.sin_addr.S_un.S_addr=htonl(INADDR_ANY);

skaddr.sin_family=AF_INET;

skaddr.sin_port=htons(6000);

if(SOCKET_ERROR==bind(m_socket,(SOCKADDR*)&skaddr,sizeof(SOCKADDR_IN)))

{

MessageBox("绑定失败!");

return FALSE;

}

if(SOCKET_ERROR==WSAAsyncSelect(m_socket,m_hWnd,UM_SOCK,FD_READ))//一旦有FD_READ事件发生,系统就会触发这个事件,
//系统就会通过UM_SOCK消息通知我们,在这个消息的响应函数中,接收数据就能收到数据。

{

MessageBox("注册网络读取事件失败!");

return FALSE;

}

return TRUE;
}
在BOOL CChatDlg::OnInitDialog()中加入
InitSocket();
在ChatDlg.h加入:
#define
UM_SOCK WM_USER+1
afx_msg void OnSock(WPARAM wParam,LPARAM lParam);
在BEGIN_MESSAGE_MAP(CChatDlg, CDialog)中加上红色的那句

//{{AFX_MSG_MAP(CChatDlg)

ON_WM_SYSCOMMAND()

ON_WM_PAINT()

ON_WM_QUERYDRAGICON()

//}}AFX_MSG_MAP

ON_COMMAND(UM_SOCK,OnSock)
END_MESSAGE_MAP()
将接收编辑框的多行属性选上。
void CChatDlg::OnSock(WPARAM wParam,LPARAM lParam)
{

switch(LOWORD(lParam))

{

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

{

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;

CString strSend;

WSABUF wsabuf;

DWORD dwSend;

int len;

CString strHostName;

SOCKADDR_IN addrTo;

HOSTENT* pHost;

if(GetDlgItemText(IDC_EDIT_HOSTNAME,strHostName),strHostName=="")

{

((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_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;

}

}

m_sock=socket(AF_INET,SOCK_DGRAM,0);

m_sock=WSASocket(AF_INET,SOCK_DGRAM,0,NULL,0,0);

这两句是对等的,不建议用第一种。此例中用的是非阻塞套按字,建议用第二种,一致。

OnRecvData中:

char rbuf[100];

wbf.buf=rbuf;

这种也可以。

发送时数据要记得多发送一个字节。

发送按钮中的:

WSABUF wbuf;

wbuf.buf=strsend.GetBuffer(length);

wbuf.len=length;

adto.sin_addr.S_un.S_addr=*((DWORD*)phost->h_addr_list[0]);在这句中,本来就是以网络字节序表示的。
若程序改为adto.sin_addr.S_un.S_addr=htonl(*((DWORD*)phost->h_addr_list[0]));是不正确的,结果出不来。
若改为adto.sin_addr.S_un.S_addr=*(/*(DWORD*)*/phost->h_addr_list[0]);也不行。
改为adto.sin_addr.S_un.S_addr=*((ULONG*)phost->h_addr_list[0]);可以。
改为adto.sin_addr.S_un.S_addr=(ULONG)*(phost->h_addr_list[0]);这句不行。
总结:必须先将字符地址变为DWORD 或者 ULONG地址,再取值。就是先变地址再取值。
当先输入IP地址(127.0.0.2)时,显示是localhost say,然后输入主机名(PC-200908301621.),显示PC-200908301621 say。

下面让时间也显示出来:

先设定一定时器和定义一个字符串类:

void CTestDlg::OnTimer(UINT nIDEvent)

{

// TODO: Add your message handler code here and/or call default

CTime t = CTime::GetCurrentTime();

m_strtime = t.Format( "%H:%M:%S" );

CDialog::OnTimer(nIDEvent);

}

将时间显示出来:

str.Format("%s %s say:%s",m_strtime,phost->h_name,wbf.buf);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: