孙鑫MFC笔记之十四--多线程同步与异步套接字编程
線程同步有三種方式:
1.????? 互斥對象涉及方法:
HANDLE hMutex=CreateMutex(NULL,FALSE,NULL);?//第二個參數為FALSE,將互斥對象聲明為空閑狀態
WaitForSingleObject(hMutex,INFINITE);?//第二個參數為INFINITE表示一直等待,直到擁有互斥對象
ReleaseMutex(hMutex);?//使用完了,將互斥對象還給操作系統
????? 具體代碼及各種情況的分析見上一章,這里就不再敘述。
?
2.????? 事件對象:
事件對象也屬于內核對象,包含一個使用計數,一個用于指明該事件是一個自動重置的事件還是一個人工重置的事件的布爾值,另一個用于指明該事件處于可用狀態還是不可用的布爾值。
有兩種不同類型的事件對象。一種是人工重置的事件,另一種是自動重置的事件。當人工重置的事件得到通知時,等待該事件的所有線程均變為可調度線程。當一個自動重置的事件得到通知時,等待該事件的線程中只有一個線程變為可調度線程。所以優先選擇自動重置的事件。
說明:CreateEvent方法第一個參數是關于的安全的結構體,一般設置為NULL;第二個參數表示是人工重置還是自動重置,TRUE代表人工重置,如果為TURE需要調用這個ResetEvent函數來人工重置為非信號狀態;第三個參數表示初始化狀態,如果為TURE初始化狀態信號為有信號的;第四個參數表示Event名稱,NULL的話,默認。
BOOL ResetEvent(
? HANDLE?hEvent?? // handle to event
);
?
?BOOL SetEvent(
? HANDLE?hEvent?? // handle to event
);//設置信號為有信號狀態
?HANDLE g_hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
?
以下代碼會出現異常:在將CreateEvent的第二個參數設置為人工重置的時候,因為等待該事件的所有線程均變為可調度線程,所以發現售票實例程序最終會出現0。所以最好還是選擇自動重置的事件。
#include?<windows.h>
#include?<iostream.h>
?
DWORD?WINAPI ThreadProc1(LPVOID lpParameter);
DWORD?WINAPI ThreadProc2(LPVOID lpParameter);
?
int?ticket=100;
HANDLE?g_hEvent;
?
void?main()
{
??????g_hEvent=CreateEvent(NULL,TRUE,TRUE,NULL);//初始化代碼應放在創建線程以前
?
????? HANDLE handle1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
????? HANDLE handle2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
????? CloseHandle(handle1);
????? CloseHandle(handle2);?
??????g_hEvent=CreateEvent(NULL,TRUE,TRUE,NULL);//放在最前面
????? Sleep(4000);
????? CloseHandle(g_hEvent);
}
?
DWORD?WINAPI ThreadProc1(LPVOID lpParameter)
{
????? while(TRUE)
????? {
?????????? WaitForSingleObject(g_hEvent,INFINITE); //第二個參數為INFINITE表示一直等待,直到擁有互斥對象
?????????? if(ticket>0)
?????????? {
???????????????? Sleep(1);
???????????????? cout<<"thread1 sale the ticket id is:"<<ticket--<<endl;
?????????? }
?????????? else
?????????????????break;
????? }
????? return 0;
}
?
DWORD?WINAPI ThreadProc2(LPVOID lpParameter)
{
????? while(TRUE)
????? {
?????????? WaitForSingleObject(g_hEvent,INFINITE);
?????????? if(ticket>0)
?????????? {
???????????????? Sleep(1);
???????????????? cout<<"thread2 sale the ticket id is:"<<ticket--<<endl;
?????????? }
?????????? else
?????????????????break;
????? }
????? return 0;
}
說明:
如果一個線程循環內部已經調用了WaitForSingleObject(g_hEvent,INFINITE);但是在單個循環完成前沒有調用SetEvent(g_hEvent)將狀態設置成可用的話,下一次進入循環時再次調用WaitForSingleObject時發現狀態不可用,所以一直等待,代碼例子將上面的代碼g_hEvent=CreateEvent(NULL,TRUE,TRUE,NULL);修改為g_hEvent=CreateEvent(NULL,FALSE,TRUE,NULL);則會出現這個問題,其結果就是僅僅線程1售出了100這張票。如果在循環退出前調用SetEvent(g_hEvent);則問題可以解決。
?
綜上所述,涉及到的方法:
HANDLE g_hEvent=CreateEvent(NULL,TRUE,FALSE,NULL);
WaitForSingleObject(g_hEvent,INFINITE); //等待事件,如果事件可用,運行下面的代碼,并且將事件狀態設置為不可用狀態,如果事件不可用,一直等待。
SetEvent(g_hEvent)? //將事件設置為可用的狀態
ResetEvent(g_hEvent) //將事件設置為不可用狀態
一般情況下WaitForSingleObject和SetEvent配對使用。
?
3.????? 關鍵代碼段:
?
關鍵代碼段(臨界區)是指一個小代碼段,在代碼能夠執行前,它必須獨占對某些資源的訪問權。
??????????? 可以將關鍵代碼段想象成電話亭資源:
?????????? CRITICAL_SECTION g_cs;
?????????? InitializeCriticalSection(&g_cs); //創建電話亭資源,一般放在構造函數中
?????????? EnterCriticalSection(&g_cs); //判斷關鍵資源所有權是否可用,可用則進入
?????????? LeaveCriticalSection(&g_cs);使用完關鍵資源后,釋放所有權
?????????? DeleteCriticalSection(&g_cs); //銷毀電話亭資源,一般放在析構函數中
????? 其中InitializeCriticalSection和DeleteCriticalSection配對使用;
????? EnterCriticalSection和LeaveCriticalSection配對使用,中間存放訪問共享資源的代碼。?
4.????? 互斥對象、事件對象與關鍵代碼段的比較
互斥對象和事件對象屬于內核對象,利用內核對象進行線程同步,速度較慢,但利用互斥對象和事件對象這樣的內核對象,可以在多個進程中的各個線程間進行同步。
關鍵代碼段是工作在用戶方式下,同步速度較快,但在使用關鍵代碼段時,很容易進入死鎖狀態,因為在等待進入關鍵代碼段時無法設定超時值。
?
5.????? 死鎖:
哲學家進餐的問題:每個哲學家手中只有一根筷子,要進餐必須有兩根,但誰也不愿意先給出自己的那根給別人。大家都處于等待狀態。
線程1擁有了臨界區對象A,等待臨界區對象B的擁有權,線程2擁有了臨界區對象B,等待臨界區對象A的擁有權,就造成了死鎖。
死鎖代碼:
#include?<windows.h>
#include?<iostream.h>
?
DWORD?WINAPI ThreadProc1(LPVOID lpParameter);
DWORD?WINAPI ThreadProc2(LPVOID lpParameter);
?
int ticket=100;
?
//創建兩個關鍵資源
CRITICAL_SECTION g_cs1;
CRITICAL_SECTION g_cs2;
?
void?main()
{
????? HANDLE handle1=CreateThread(NULL,0,ThreadProc1,NULL,0,NULL);
????? HANDLE handle2=CreateThread(NULL,0,ThreadProc2,NULL,0,NULL);
????? CloseHandle(handle1);
????? CloseHandle(handle2);???
????? InitializeCriticalSection(&g_cs1);//初始化要放在前面
????? InitializeCriticalSection(&g_cs2);
?
????? Sleep(4000);
????? DeleteCriticalSection(&g_cs1);
????? DeleteCriticalSection(&g_cs2);
}
?
DWORD?WINAPI ThreadProc1(LPVOID lpParameter)
{
????? while(TRUE)
????? {
?????????? EnterCriticalSection(&g_cs1);
?????????? Sleep(1);
?????????? EnterCriticalSection(&g_cs2);
?????????? if(ticket>0)
?????????? {
???????????????? Sleep(1);
???????????????? cout<<"thread1 sale the ticket id is:"<<ticket--<<endl;
?????????? }
?????????? else
?????????????????break;
?????????? LeaveCriticalSection(&g_cs1);
?????????? LeaveCriticalSection(&g_cs2);
????? }
????? return 0;
}
?
DWORD?WINAPI ThreadProc2(LPVOID lpParameter)
{
????? while(TRUE)
????? {
?????????? EnterCriticalSection(&g_cs2);
?????????? Sleep(1);
?????????? EnterCriticalSection(&g_cs1);
?????????? if(ticket>0)
?????????? {
???????????????? Sleep(1);
???????????????? cout<<"thread2 sale the ticket id is:"<<ticket--<<endl;
?????????? }
?????????? else
?????????????????break;
?????????? LeaveCriticalSection(&g_cs1);
?????????? LeaveCriticalSection(&g_cs2);
????? }
????? return 0;
}
?
說明:首先線程1得到資源1的所有權,然后睡眠1毫秒,線程1就讓出了執行權,這個時候線程2得到執行權,運行,得到資源2的所有權,線程2然后睡眠1毫秒,線程2就讓出了執行權,這個時候線程1得到執行權,線程1繼續執行,想得到資源2的資源,但發現資源2被線程1所占用,等待。當線程1的事件片過了以后,線程2得到執行權,繼續執行,想得到資源1的所有權,但發現資源1被線程1占用,所以也繼續等待,這樣線程1和線程2都互相等待,造成死鎖。
?
1.??????異步套接字編程:
Windows套接字在兩種模式下執行I/O操作,阻塞和非阻塞。在阻塞模式下,在I/O操作完成前,執行操作的Winsock函數會一直等待下去,不會立即返回程序(將控制權交還給程序)。而在非阻塞模式下,Winsock函數無論如何都會立即返回。采用異步套接字,可有效改善程序的運行性能。
Windows Sockets為了支持Windows消息驅動機制,使應用程序開發者能夠方便地處理網絡通信,它對網絡事件采用了基于消息的異步存取策略。
Windows Sockets的異步選擇函數WSAAsyncSelect()提供了消息機制的網絡事件選擇,當使用它登記的網絡事件發生時,Windows應用程序相應的窗口函數將收到一個消息,消息中指示了發生的網絡事件,以及與事件相關的一些信息。
??????在上一章中編寫的Chat程序中,因為接收程序放在了一個線程中,所以雖然它是阻塞的,也沒有影響到主線程的運行性能。?
2.??????編寫基于異步套接字的聊天室程序:
?
相關函數:
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 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,套接字屬性的描述。
?
?
?
a.??????因為MFC自帶的AfxSocketInit函數初始化支持的是1.1版本的套接字,不適合異步套接字,我們需要調用的是Winsock2版本的套接字,那么加載套接字庫的過程只能使用WSAStartup了。在CChatApp的InitInstance初始化函數中添加:
?
WORD wVersionRequested;
??????WSADATA wsaData;
??????int?err;?????
??????wVersionRequested = MAKEWORD( 2, 2 );????
??????err = WSAStartup( wVersionRequested, &wsaData );
??????if?( err != 0 ) {
???????????return;
??????}?????????
??????if?( LOBYTE( wsaData.wVersion ) != 2 ||
????????HIBYTE( wsaData.wVersion ) != 2 ) {??????????
???????????WSACleanup( );
???????????return;
}
b.??????在StdAfx.h里添加#include <winsock2.h>,在setting里添加ws2_32.lib庫文件。
c.??????給CChatApp類添加析構函數,在其中添加WSACleanup來終止對套接字庫的使用。
d.??????給CChatDlg類添加成員變量SOCKET m_socket,并在構造函數中初始化為0
e.??????給CChatDlg類添加析構函數,添加:
if(m_socket) //判斷socket是否有值
??????closesocket(m_socket);
f.???????創建初始化函數InitSocket(),代碼如下:
說明:在Winsock2版本中提供的WSASocket這樣一個擴展方法用于創建套接字,對應于socket方法;bind方法在winsock2中沒有提供相應的擴展方法。然后調用WSAAsyncSelect方法請求一個windows基于消息的網絡事件通知。
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(1234);
??
???int?retVal;
???retVal=bind(m_socket,(SOCKADDR*)&addrSock,sizeof(SOCKADDR));
???if(SOCKET_ERROR==retVal)
???{
????????MessageBox("套接字綁定失敗!");
????????return?FALSE;
???}
說明:WSAAsyncSelect方法第二個參數表示網絡事件發生時用來接收消息的窗口,第三個參數表示處理響應的消息,第四個參數表示網絡事件類型,采用或操作。我們當前采用讀這樣一個事件,網絡上一旦有數據到來的時候就會觸發這個事件,系統就會通過我們自定義的消息UM_SOCK來通知我們進行處理if(SOCKET_ERROR==WSAAsyncSelect(m_socket,m_hWnd,UM_SOCK,FD_READ))
????????????{
?????????????????MessageBox("注冊網絡讀取事件失敗!");
?????????????????return?FALSE;
???};
return?TRUE;
g.??????消息響應函數的處理:
1.??????創建自定以的消息UM_SOCK,注意:在消息響應函數的申明中還是要添加WPARAM和LPARAM參數,因為網絡上的數據是通過這兩個參數傳遞給消息響應函數進行處理的。
2.??????參看MSDN中WSAAsyncSelect方法的說明如下:
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?lParamspecifies the network event that has occurred. The high word of?lParam?contains any error code.
3.??????WSARecvFrom函數的第二個參數可表示一個WSABUF的結構體數組,可用于存放多個從網絡上接收到的信息塊,當然也可以將所有信息放在一個結構體中,然后將自己關心的信息塊取出,但這樣做比較麻煩,可以直接用WSABUF結構體數組接收不同信息的塊即可。(沒有具體的實際操作經驗)
在消息響應函數中添加如下代碼
?
?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結構體包含一個緩沖區的指針和緩沖區的長度。
typedef struct __WSABUF {
? u_long??????len;
? char FAR??? *buf;
} WSABUF, FAR * LPWSABUF;
dwBufferCount, lpBuffers數組中WSABUF結構體的數目。
lpNumberOfBytesRecvd,[out],如果接收操作立即完成,則為一個指向本次調用所接收的字節數的指針。
lpFlags,[in, out],一個指向標志位的指針。
lpFrom,[out],可選指針,指向重疊操作完成后存放源地址的緩沖區。
lpFromlen,[in, out],指向from緩沖區大小的指針,僅當指定了lpFrom才需要。
lpOverlapped,一個指向WSAOVERLAPPED結構體的指針(對于非重疊套接字則忽略)。
lpCompletionRoutine,一個指向接收操作完成時調用的完成例程的指針(對于非重疊套接字則忽略)。
?
?
switch(LOWORD(lParam)) {?//lParam的低字節指明網絡事件的類型
??????case?FD_READ:?//我們當前只有讀取這樣一個事件,這是在WSAAsyncSelect中設定的
???????????WSABUF wsabuf;
???????????wsabuf.buf=new?char[200];?//網絡上接收到的數據
???????????wsabuf.len=200;
???????????DWORD dwRead;
???????????DWORD dwFlag=0;
???????????SOCKADDR_IN addrFrom;
???????????int?len=sizeof(addrFrom);
???????????CString str;
????????????if(SOCKET_ERROR==WSARecvFrom(m_socket,&wsabuf,1,&dwRead,&dwFlag,(SOCKADDR*)&addrFrom,&len,NULL,NULL))
???????????{
?????????????????//下面的消息框基本不會運行,因為WSARecvFrom方法是在有網絡數據的情況下才會被調用的,所以運行到這段,基本是有數據的,做這樣一個判斷,只是出于編程風格一致而已
?????????????????MessageBox("接收數據失敗!");
?????????????????return;
???????????}
??????str.Format("from %s said:%s",inet_ntoa(addrFrom.sin_addr),wsabuf.buf);
???????????CString temp;
???????????GetDlgItemText(IDC_EDIT_RECV,temp);
???????????temp+="/r/n"+str;
???????????SetDlgItemText(IDC_EDIT_RECV,temp);
???????????break;
}
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,一個指向接收操作完成時調用的完成例程的指針(對于非重疊套接字則忽略)。
?
DWORD dwIP;?//控件上填寫的IP地址
???CString strSend;?//需要發送的信息內容
???WSABUF wsbuf;?//需要發送的信息內容
???DWORD dwSend;
???((CIPAddressCtrl*)GetDlgItem(IDC_IPADDRESS1))->GetAddress(dwIP);
???GetDlgItemText(IDC_EDIT_SEND,strSend);
?
???SOCKADDR_IN addrTo;
???addrTo.sin_addr.S_un.S_addr=htonl(dwIP);
???addrTo.sin_family=AF_INET;
???addrTo.sin_port=htons(1234);
?
???//GetBuffer函數將CString類型轉換為char*類型
???wsbuf.buf=strSend.GetBuffer(strSend.GetLength());
???wsbuf.len=strSend.GetLength()+1;?//多一個字節用于存放結束操作符
???if(SOCKET_ERROR==WSASendTo(m_socket,&wsbuf,1,&dwSend,0,(SOCKADDR*)&addrTo,sizeof(SOCKADDR),NULL,NULL))
???{
????????MessageBox("發送數據失敗!");
????????return;
???}
???else
???{
????????SetDlgItemText(IDC_EDIT_SEND,"");
}
i.????????綜上所述,創建一個基于winsock2版本的異步套接字的網絡聊天室程序有以下幾個步驟:
1.??????調用WSAStartup加載套接字庫
2.??????調用WSASocket創建套接字
3.?????調用WSAAsyncSelect請求基于windows消息的網絡事件通知
4.??????創建自定義的消息響應函數,來處理捕獲的網絡事件
5.??????在消息響應函數內部調用WSARecvFrom來處理接收到的數據
6.??????調用WSASendTo處理發送數據?
?
3.通過主機名稱實現聊天:
??????? //IP轉換為主機名稱
??????? //str.Format("from %s said:%s",inet_ntoa(addrFrom.sin_addr),wsabuf.buf);修改為以下代碼
?HOSTENT *pHost;
?pHost = gethostbyaddr((char*)&addrFrom.sin_addr.S_un.S_addr,4,AF_INET);????
?str.Format("from %s said:%s",pHost->h_name,wsabuf.buf);?
?
??????? //主機名稱轉換為IP
?HOSTENT * pHost;
??????? SOCKADDR_IN addrTo;?
?CString strHostName;
?
?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]);
?}
??
4.??????小結:
當前程序將消息的接收和發送放在了同一個線程中,即主線程中。如果采用先前使用過的阻塞套接字的話,程序會因為接收函數的調用導致主線程的暫停運行,就無法及時的發送消息了。但是采用異步套接字可使得發送和接收放在同一個線程中而不會有相互的影響。
如果采用異步套接字加上多線程編程,則大大會提高網絡運用程序的性能。
在第十四課中講winsock1.1的編程中一般將接收函數放在一個while循環中,來使得程序一直處于接收響應狀態,在異步套接字中,利用了在程序初始化的時候調用了WSAAsyncSelect方法來聲明程序的網絡的事件有相應的自定義消息來處理,其真正的核心部分還是封裝在MFC中
本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/roger_ge/archive/2008/09/09/2903337.aspx
總結
以上是生活随笔為你收集整理的孙鑫MFC笔记之十四--多线程同步与异步套接字编程的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 运动估计简介
- 下一篇: SendMessage、PostMess