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