鳴謝
感謝PiggyXP兄的雄文《手把手叫你玩轉網(wǎng)絡(luò )編程系列之三——完成端口(Completion Port)詳解》提供的思路
C++11標準提出來(lái)有些年頭了,十一放假沒(méi)事研究了一下IOCP,想著(zhù)能不能用C++11實(shí)現一個(gè)高性能的服務(wù)器。當然,目前有許多十分成熟的C++網(wǎng)絡(luò )庫,比如ACE,asio等等。但是如果想深入了解其本質(zhì),在Windows平臺下就必須了解Socket結合IOCP的使用原理。
本文盡可能把筆者在使用C++11實(shí)現IOCP服務(wù)器的過(guò)程中遇到的困難和問(wèn)題展現給大家,讓大家學(xué)習起來(lái)少走些彎路。由于代碼比較底層,所以有些細節希望大家在看本文和代碼的時(shí)候能夠揣摩和理解。本文假定讀者總體把握了PiggyXP原文的相關(guān)內容并具有相當的Window編程的相關(guān)知識(熟悉WinSock2庫基本函數的使用,Windows多線(xiàn)程的基本概念等)、C++11/03編程基礎(STL,仿函數等)。
在每一節標題后都有箭頭指向目錄,文檔某些位置可能會(huì )有返回箭頭(返回到可能你在閱讀的地方),希望能幫助大家更好的理解本文。
本文代碼遵循Apache License 2.0協(xié)議,歡迎各位大神拍磚。分享帶來(lái)進(jìn)步,如需轉載請標明作者和出處,謝謝!
溫馨提示:由于筆者水平有限,雖經(jīng)過(guò)仔細調試,但本文代碼仍然可能存在筆者未知的Bug或者性能缺陷。請大家發(fā)現問(wèn)題后能夠及時(shí)聯(lián)系我,讓我們共同進(jìn)步。
| 軟件/系統 | 版本 |
|---|---|
| 操作系統 | Windows 10 v1607 x64 |
| IDE/編譯器 | Visual Studio 2015/CL 19 |
| Win SDK | 10.0.10240 |
| 編程語(yǔ)言 | C++11 |
本節參考文獻
Nasarre C, Richter J. Windows? via C/C++[M]. Pearson Education, 2007: 291-316.
在生活中,異步的概念是很常見(jiàn)的。比如你洗衣服時(shí)突然女朋友(程序員有女朋友?)來(lái)了,你從洗衣間出去招待,而洗衣機則按照你的指令繼續在工作。當你招呼完女朋友回到洗衣間的時(shí)候,衣服已經(jīng)洗好了。也就是在女朋友來(lái)的時(shí)間點(diǎn),你與洗衣機分離,它按照你的指令在完成工作,而你卻可以處理其他更需要處理的事情。當你處理完回來(lái)后,洗衣機可能早已經(jīng)完成了它的工作,你只需要將衣服取出晾起來(lái)就可以了。而同步就是你家沒(méi)有洗衣機,當女朋友來(lái)的時(shí)候要么中斷洗衣服去招待女朋友,要么讓女朋友等待自己把衣服洗完,一件事情只能在另一件事情之后發(fā)生。這樣,大家就能明顯看出來(lái)有臺洗衣機的好處了。
不過(guò)如何知道衣服洗完了呢?Windows牌洗衣機給我們提供了這么四種方式:
| 方式 | 解釋 | 相關(guān)技術(shù) |
|---|---|---|
| LED燈 | 洗完一件衣服就亮燈,但只有一個(gè)燈,其他人可以幫忙處理 | 觸發(fā)設備內核對象 |
| 高級LED燈 | 洗完一件衣服就亮燈,可以有多個(gè)燈,其他人可以幫忙處理 | 觸發(fā)事件內核對象 |
| 發(fā)送短信 | 洗完一件衣服就發(fā)送一條短信,有一個(gè)短信列表,但只有你能夠處理 | 可提醒IO(APC) |
| 群發(fā)短信 | 洗完一件衣服就發(fā)送一條短信,有一個(gè)短信列表,其他人可以幫忙處理 | IO完成端口(IOCP) |
這樣,大家就很明白IOCP的好處了:不需要去時(shí)刻看著(zhù)燈亮不亮;短信到了可以去處理也可以不去處理;不僅你能處理,還有家人也能幫你處理。
觸發(fā)設備內核對象、觸發(fā)事件內核對象和可提醒IO就不展開(kāi)討論了,有興趣的朋友可以查閱本節列出的參考文獻,下面進(jìn)入正題。
這一小節可能比較難,希望大家能夠耐心看下去,因為要真正掌握IOCP就必須弄清楚它內在的原理。先給出IOCP的狀態(tài)機,如圖1所示:
下面給出圖中各組件的相關(guān)說(shuō)明:
| 組件 | 簡(jiǎn)要解釋 |
|---|---|
| 等待隊列 | 當線(xiàn)程池中的某線(xiàn)程在等待IO操作時(shí)(調用GetQueuedCompletionStatus函數),IOCP將線(xiàn)程加入等待隊列。IOCP在IO操作完成后將返回結果加入完成隊列,由等待隊列中的最后一個(gè)加入的線(xiàn)程處理。 |
| 已釋放列表 | 當等待的線(xiàn)程處理完IO操作后或是從暫停狀態(tài)被喚醒都會(huì )加入此列表。 當線(xiàn)程再次調用 GetQueuedCompletionStatus函數將使自己再次加入等待隊列;將自身掛起將加入已暫停列表。 |
| 已暫停列表 | 當已釋放列表中的線(xiàn)程掛起時(shí)將加入已暫停列表;當掛起線(xiàn)程被激活時(shí)線(xiàn)程加入已釋放列表。 |
| 完成隊列 | IOCP完成指定IO操作后將執行結果插入完成隊列。這個(gè)隊列時(shí)先進(jìn)先出的。 |
| IOCP設備列表 | 即要進(jìn)行異步IO操作的設備列表(可以是文件,也可以是套接字),所有的IO操作都圍繞這些設備進(jìn)行。 |
這樣,整個(gè)IOCP服務(wù)器創(chuàng )建的流程就很明了了:?
- 創(chuàng )建一個(gè)新的完成端口,處理所有的IO請求。
- 創(chuàng )建一個(gè)線(xiàn)程池,此時(shí)線(xiàn)程處于
已釋放列表。- 創(chuàng )建一個(gè)
Socket并將其綁定在創(chuàng )建的完成端口上,作為IO操作的實(shí)體。利用這個(gè)套接字進(jìn)行Listen操作,并向第1步創(chuàng )建的完成端口中投遞Accept消息,將第2步創(chuàng )建線(xiàn)程置于等待隊列中等待客戶(hù)端連接。- 當客戶(hù)端連接后,IOCP將在
IO完成隊列插入Accept,等待隊列中的線(xiàn)程將得到Accept,并創(chuàng )建新的Socket作為與客戶(hù)端通信的套接字,并將其綁定在第1步創(chuàng )建好的完成端口上。- 此后,無(wú)論是
Recv,Send都照此步驟進(jìn)行即可。
這里有幾個(gè)細節需要注意:
1. 最合適的線(xiàn)程數應當是多于處理器核心數的
多線(xiàn)程優(yōu)化理論告誡我們,為了避免ring0與ring3之間的上下文切換,我們應當將線(xiàn)程數設置為處理器核數。但是微軟在設計IOCP的時(shí)候想到了這樣一個(gè)問(wèn)題:考慮到線(xiàn)程掛起,如果按照理論值設置線(xiàn)程數,將有可能出現實(shí)際工作線(xiàn)程數小于CPU所能接受的最大工作線(xiàn)程數,這樣就無(wú)法有效發(fā)揮多線(xiàn)程的優(yōu)勢。因此,最理想的線(xiàn)程數量應當多于處理器核心數的,經(jīng)驗值為兩倍核心數。
2. 等待隊列是后入先出的
之所以這樣設計也是出于性能調優(yōu)的考慮。當某線(xiàn)程處理完某批IO數據后重新加入等待隊列,由于LIFO機制,當完成隊列中又存在有新的IO數據時(shí),該線(xiàn)程將會(huì )優(yōu)先處理數據。這樣可能會(huì )導致某些線(xiàn)程一直處于等待狀態(tài),這樣Windows就可以將其換出內存節約空間。
3. 投遞
所謂投遞其實(shí)就是利用AcceptEx,WSARecv和WSASend等函數在IO完成端口中進(jìn)行異步操作。形象來(lái)說(shuō)就是你向洗衣機輸入參數的過(guò)程,后續工作由洗衣機(WinSock2)完成。
本節參考文獻
Microsoft. I/O Completion Ports[EB/OL]. https://msdn.microsoft.com/en-us/library/aa365198(VS.85).aspx
Microsoft. Windows Sockets 2[EB/OL]. https://msdn.microsoft.com/en-us/library/windows/desktop/ms740673(v=vs.85).aspx
Russinovich M E, Solomon D A, Ionescu A. Windows internals[M]. Pearson Education, 2012: 56-58.
關(guān)于常規的IO完成端口A(yíng)PI主要有以下三個(gè):
創(chuàng )建和關(guān)聯(lián)IO完成端口函數CreateIoCompletionPort,該函數在創(chuàng )建完成端口和關(guān)聯(lián)設備(文件設備,套接字等)時(shí)使用。
HANDLE WINAPI CreateIoCompletionPort( _In_ HANDLE FileHandle, _In_opt_ HANDLE ExistingCompletionPort, _In_ ULONG_PTR CompletionKey, _In_ DWORD NumberOfConcurrentThreads );
獲取完成隊列狀態(tài)函數GetQueuedCompletionStatus,該函數在線(xiàn)程池線(xiàn)程函數中使用。?
BOOL WINAPI GetQueuedCompletionStatus( _In_ HANDLE CompletionPort, _Out_ LPDWORD lpNumberOfBytesTransferred, _Out_ PULONG_PTR lpCompletionKey, _Out_ LPOVERLAPPED * lpOverlapped, _In_ DWORD dwMilliseconds );
在完成隊列中插入消息函數PostQueuedCompletionStatus,該函數在給線(xiàn)程傳遞退出參數時(shí)使用。?
BOOL WINAPI PostQueuedCompletionStatus( _In_ HANDLE CompletionPort, _In_ DWORD dwNumberOfBytesTransferred, _In_ ULONG_PTR dwCompletionKey, _In_opt_ LPOVERLAPPED lpOverlapped );以上函數的詳細用法在參考文獻及piggyXP的文章中可以找到,故不再贅述。
在編程過(guò)程中主要考慮以下幾個(gè)問(wèn)題:
1.
CreateIoCompletionPort函數的設計問(wèn)題
按照設計模式最基礎的原則即單一職責原則,這個(gè)函數設計是存在缺陷的。事實(shí)上很多Windows API都或多或少存在此問(wèn)題,筆者印象比較深刻的是NetBIOS的系列函數。理想的設計是自己再抽象兩個(gè)函數,即創(chuàng )建完成端口一個(gè)函數,綁定完成端口一個(gè)函數??梢赃@樣設計:
創(chuàng )建一個(gè)新的完成端口函數CreateNewIoCompletionPort,該函數在初始化時(shí)使用。
/*** Create completion port*/inline auto CreateNewIoCompletionPort( DWORD NumberOfConcurrentThreads = 0 ) { return CreateIoCompletionPort( INVALID_HANDLE_VALUE, nullptr, 0, NumberOfConcurrentThreads );}
設備與完成端口綁定函數AssociateDeviceWithCompletionPort,該函數在完成端口建立后與IO設備綁定時(shí)使用。?
/*** Associate device with completion port*/inline auto AssociateDeviceWithCompletionPort( HANDLE hCompPort, HANDLE hDevice, DWORD dwCompKey ) { return CreateIoCompletionPort( hDevice, hCompPort, dwCompKey, 0 ) == hCompPort;}2. 線(xiàn)程池線(xiàn)程退出問(wèn)題
由于在程序中使用了線(xiàn)程池,對于每一個(gè)線(xiàn)程而言如何不留痕跡地結束是一個(gè)很有技巧性的問(wèn)題。一種優(yōu)雅的方法是使用PostQueuedCompletionStatus函數給完成端口傳遞退出完成鍵(CompletionKey)。由于線(xiàn)程只有可能在等待隊列、已釋放列表和已暫停列表中,且設計線(xiàn)程函數時(shí)均會(huì )循環(huán)調用GetQueuedCompletionStatus函數,因此最終所有線(xiàn)程都會(huì )轉移到等待隊列中去。
有的讀者會(huì )考慮到等待隊列的LIFO特性,其實(shí)只要我們設計線(xiàn)程函數時(shí)首先判斷傳入的完成鍵是否為退出的特定信號,檢測到自行退出即可。我們在主線(xiàn)程退出時(shí)在完成端口中傳入創(chuàng )建線(xiàn)程數量個(gè)推出信號,由于是完成隊列是順序存取,只要線(xiàn)程函數設計合理,可以保證每一個(gè)線(xiàn)程函數都可以收到退出消息。不會(huì )發(fā)生piggyXP考慮的收不到信息的情況。
更深入的討論高級程序員參考
筆者深入分析了GetQueuedCompletionStatus函數(由Kernel32.dll轉發(fā),在KernelBase.dll中實(shí)現),發(fā)現其內部準備好各項參數后調用了NtRemoveIoCompletion函數(由ntdll.dll轉發(fā),在內核ntoskrnl.exe中實(shí)現)。這樣就很明白了,其實(shí)就是在完成隊列中取出一個(gè)數據。繼續對
NtRemoveIoCompletion函數進(jìn)行分析,發(fā)現在內部調用了IoRemoveIoCompletion,繼續深究下去發(fā)現其主要功能調用了KeRemoveQueueEx函數,而在該函數內部進(jìn)行了無(wú)鎖同步:
if ( _interlockedbittestandset( ... ) ) { do { do KeYieldProcessorEx( ... ); while ( ... ); } while ( _interlockedbittestandset( ... ) );}這樣就能保證APC交付時(shí),只有一個(gè)線(xiàn)程可以訪(fǎng)問(wèn)到完成隊列。因此,只要在設計過(guò)程中一次只取出一個(gè)完成的數據,就不會(huì )出現問(wèn)題。當然,如果想更高效的處理數據(比如調用
GetQueuedCompletionStatusEx)又想通過(guò)PostQueuedCompletionStatus方式退出的話(huà),就可能需要特殊處理。比如像piggyXP一樣設計一個(gè)信號量,或者接收到退出信號后在退出之前向完成隊列中再Post一個(gè)退出信號等等。如果想要更加深入的了解其中的運作機理,大家可以去看看WRK或者是React OS的源碼。當然,這些代碼時(shí)代都比較久遠了,可能細節上和現在的Windows實(shí)現不太一樣,但是也能說(shuō)明問(wèn)題。
P.S.
在Windows Vista以上操作系統,將完成端口的句柄直接關(guān)閉將取消所有關(guān)聯(lián)的IO操作,關(guān)聯(lián)IO端口的所有線(xiàn)程調用GetQueuedCompletionStatus會(huì )放棄等待并立即返回FALSE,這時(shí)調用GetLastError獲取錯誤碼時(shí),會(huì )返回ERROR_INVALID_HANDLE。檢測到這一情況就可以退出了。小插曲
在分析Windows 10內核的時(shí)候在Explorer中可以看到ntoskrnl,而在IDA中看不到。最后只得將其復制到其他地方才進(jìn)行了分析,感嘆一句微軟套路深。
3. 完成鍵(CompletionKey)和重疊結構(Overlapped)的設置問(wèn)題?
這里可能是理解完成端口的一個(gè)難點(diǎn),至少筆者在學(xué)習的時(shí)候在這里停頓了一段時(shí)間。
首先說(shuō)說(shuō)完成鍵。這個(gè)參數是為了給線(xiàn)程池中的線(xiàn)程通信而設計的,也就是說(shuō)當調用前文所述AssociateDeviceWithCompletionPort時(shí)傳入的完成鍵將會(huì )傳給調用GetQueuedCompletionStatus的線(xiàn)程。這樣,主線(xiàn)程就可以通過(guò)這兩個(gè)函數與線(xiàn)程池中的線(xiàn)程進(jìn)行通信。同樣注意到完成鍵是一個(gè)DWORD類(lèi)型,也可以給它傳入一個(gè)結構體的地址。
而重疊結構是在IO處理時(shí)傳遞給相應IO函數的數據載體。這個(gè)結構很有用,但本文不再展開(kāi)說(shuō)明,有興趣的朋友可以查看參考文獻相應部分。C/C++程序員應該都知道這樣一個(gè)事實(shí):結構體的第一個(gè)成員的地址和結構體的地址是相同的。所以,我們可以定義一個(gè)結構體(或者是一個(gè)C++類(lèi)),將重疊結構作為第一個(gè)成員,在IO處理時(shí),將我們定義的結構傳入。這樣,IO函數處理它自身需要的重疊結構信息,而我們可以在其中夾帶私貨。為什么要這么做呢?因為在我們在線(xiàn)程函數中可能需要一些其他的數據,這樣就可以通過(guò)這種辦法傳進(jìn)去。
于是我們就明白了:完成鍵與線(xiàn)程有關(guān)而重疊結構與IO有關(guān)。我們需要完成鍵給線(xiàn)程傳遞參數,需要重疊結構(以及夾帶的私貨)來(lái)完成IO操作。
至于這些怎樣與Socket結合,請瀏覽下一節內容。
更深入的討論高級程序員參考
在piggyXP的博文中提到了一個(gè)“神奇的宏”:CONTAINING_RECORD。這個(gè)宏廣泛應用于驅動(dòng)編程中,用于獲取在知道結構體某成員地址的情況下推知整個(gè)結構體地址的場(chǎng)景中。具體定義如下:
/*** Calculate the address of the base of the structure given its type, and an* address of a field within the structure.*/#define CONTAINING_RECORD(address, type, field) ((type *)( (PCHAR)(address) - (ULONG_PTR)(&((type *)0)->field)))這個(gè)是帶有濃郁C風(fēng)格、充滿(mǎn)trick的一個(gè)宏。能進(jìn)行深入討論的朋友一看就明白,就不班門(mén)弄斧了。值得注意的是,使用這個(gè)宏的時(shí)候對成員是否是結構體的第一個(gè)成員沒(méi)有限制。
主要使用的API有如下6個(gè):
創(chuàng )建套接字函數WSASocket,在創(chuàng )建OVERLAPPED套接字時(shí)使用。
注意
WSASocket是一個(gè)宏定義,在MBCS環(huán)境下定義為WSASocketA,在UNICODE環(huán)境下定義為WSASocketW。
SOCKET WSAAPI WSASocketW ( // WSASocketA for MBCS _In_ int af, _In_ int type, _In_ int protocol, _In_opt_ LPWSAPROTOCOL_INFOW lpProtocolInfo, // LPWSAPROTOCOL_INFOA for MBCS _In_ GROUP g, _In_ DWORD dwFlags );綁定函數bind,在服務(wù)器初始化時(shí)使用。
int WSAAPI bind( _In_ SOCKET s, _In_reads_bytes_(namelen) const struct sockaddr FAR * name, _In_ int namelen );監聽(tīng)函數listen,在等待客戶(hù)端連接監聽(tīng)時(shí)使用。
int WSAAPI listen( _In_ SOCKET s, _In_ int backlog );控制套接字函數WSAIoctl,在獲取函數指針時(shí)使用。
int WSAAPI WSAIoctl( _In_ SOCKET s, _In_ DWORD dwIoControlCode, _In_reads_bytes_opt_(cbInBuffer) LPVOID lpvInBuffer, _In_ DWORD cbInBuffer, LPVOID lpvOutBuffer, _In_ DWORD cbOutBuffer, _Out_ LPDWORD lpcbBytesReturned, _Inout_opt_ LPWSAOVERLAPPED lpOverlapped, _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
微軟擴展的accept函數AcceptEx,用于接受用戶(hù)接入并獲取第一組傳輸的數據,代替accept使用。?
BOOL PASCAL AcceptEx ( _In_ SOCKET sListenSocket, _In_ SOCKET sAcceptSocket, PVOID lpOutputBuffer, _In_ DWORD dwReceiveDataLength, _In_ DWORD dwLocalAddressLength, _In_ DWORD dwRemoteAddressLength, _Out_ LPDWORD lpdwBytesReceived, _Inout_ LPOVERLAPPED lpOverlapped );微軟擴展的配合解析AcceptEx函數返回值使用的函數GetAcceptExSockaddrs,需要獲取第一組數據的時(shí)候使用。
void GetAcceptExSockaddrs ( _In_ PVOID lpOutputBuffer, _In_ DWORD dwReceiveDataLength, _In_ DWORD dwLocalAddressLength, _In_ DWORD dwRemoteAddressLength, _Out_ LPSOCKADDR *LocalSockaddr, _Out_ LPINT LocalSockaddrLength, _Out_ LPSOCKADDR *RemoteSockaddr, _Out_ LPINT RemoteSockaddrLength);
異步接受數據函數WSARecv,在接收數據時(shí)使用。?
int WSAAPI WSARecv( _In_ SOCKET s, _In_reads_(dwBufferCount) __out_data_source(NETWORK) LPWSABUF lpBuffers, _In_ DWORD dwBufferCount, _Out_opt_ LPDWORD lpNumberOfBytesRecvd, _Inout_ LPDWORD lpFlags, _Inout_opt_ LPWSAOVERLAPPED lpOverlapped, _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );
異步接受數據函數WSASend,在接收數據時(shí)使用。?
int WSAAPI WSASend( _In_ SOCKET s, _In_reads_(dwBufferCount) LPWSABUF lpBuffers, _In_ DWORD dwBufferCount, _Out_opt_ LPDWORD lpNumberOfBytesSent, _In_ DWORD dwFlags, _Inout_opt_ LPWSAOVERLAPPED lpOverlapped, _In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine );以上函數的詳細用法在參考文獻及piggyXP的文章中可以找到,故不再贅述。
在編程過(guò)程中主要考慮以下幾個(gè)問(wèn)題:
1.
AcceptEx和GetAcceptExSockaddrs函數的調用問(wèn)題
在實(shí)際使用中我們可以發(fā)現,調用這兩個(gè)函數無(wú)一不是利用了WSAIoctl返回的函數指針。筆者在MSDN中也找到了這樣的說(shuō)法:
“The function pointer for the AcceptEx / GetAcceptExSockaddrs function must be obtained at run time by making a call to the WSAIoctl function with the SIO_GET_EXTENSION_FUNCTION_POINTER opcode specified. “
因此,我們在使用這兩個(gè)函數之前必須通過(guò)WSAIoctl來(lái)獲取這兩個(gè)函數的指針加以調用。
更深入的討論高級程序員參考
事實(shí)上筆者發(fā)現,在mswsock.dll中是導出了這兩個(gè)函數的。那為什么微軟在MSDN中沒(méi)有說(shuō)到呢,非要用如此麻煩的方式去調用AcceptEx和GetAcceptExSockaddrs這兩個(gè)函數?
mswsock.dll其實(shí)也只是一個(gè)轉發(fā)器,真實(shí)的函數在另外的地方。在AcceptEx函數內部也會(huì )調用WSAIoctl(在ws2_32.dll中實(shí)現)來(lái)獲取真實(shí)的函數地址。有一個(gè)非常有意思的地方,
AcceptEx函數除了尋找自己的真實(shí)函數地址以外,還回去尋找GetAcceptExSockaddrs函數的地址,同時(shí)進(jìn)行設置;在導出的GetAcceptExSockaddrs函數內部不會(huì )再去尋找自身實(shí)現的地址,而是使用AcceptEx函數設置的地址,如果地址為空則將后四個(gè)傳入的參數全部置零,有興趣的朋友可以嘗試一下。所以使用導出的
AcceptEx而不通過(guò)指針從理論上也是可以的,在使用導出的GetAcceptExSockaddrs之前務(wù)必要使用導出的AcceptEx來(lái)設置內部指針,而且并不是說(shuō)使用導出的函數效率低才使用函數指針獲取函數實(shí)現地址??赡艿脑蚴敲總€(gè)Windows版本的實(shí)現驅動(dòng)可能不同,對上的接口需要mswsock.dll來(lái)保持一致。另外,在使用這兩個(gè)函數時(shí)要注意在傳遞
SOCKADDR_IN結構體大小時(shí)要加上16,與具體實(shí)現相關(guān),原因不明。
2. 設置各函數完成鍵和重疊結構體的問(wèn)題
接完成鍵(CompletionKey)和重疊結構(Overlapped)的設置問(wèn)題討論。在本文程序中,要設置完成鍵和重疊結構體的主要有以下6個(gè)函數,如表4所示:
| 函數 | 需要設置的內容 | 相關(guān)解釋 |
|---|---|---|
| AssociateDeviceWithCompletionPort | 完成鍵 | 初始化時(shí)將完成端口和線(xiàn)程綁定時(shí)需要使用 |
| GetQueuedCompletionStatus | 完成鍵和重疊結構 | 線(xiàn)程獲取參數與IO狀態(tài)時(shí)使用 |
| PostQueuedCompletionStatus | 完成鍵和重疊結構 | 傳遞線(xiàn)程參數與設置IO狀態(tài)時(shí)使用 |
| AcceptEx | 重疊結構 | 在異步接受客戶(hù)端接入時(shí)使用 |
| WSARecv | 重疊結構 | 在異步接收消息時(shí)使用 |
| WSASend | 重疊結構 | 在異步發(fā)送消息時(shí)使用 |
大家一看就明白了,AssociateDeviceWithCompletionPort是主線(xiàn)程將創(chuàng )建好的完成端口與IO設備綁定時(shí)調用的,只需要完成鍵;GetQueuedCompletionStatus函數是線(xiàn)程池中工作線(xiàn)程調用的,因此要獲取完成鍵和重疊結構;PostQueuedCompletionStatus函數要傳遞參數和設置IO狀態(tài)到完成隊列中去,因此也需要兩個(gè);AcceptEx、WSARecv和WSASend函數是用來(lái)進(jìn)行IO操作(網(wǎng)絡(luò )操作)的,因此只需要和網(wǎng)絡(luò )IO設備打交道,只需設置重疊結構。
注意到前述討論中的問(wèn)題,可以設計這樣一個(gè)結構體充當重疊結構夾帶私貨:
using IO_CONTEXT = struct _IO_CONTEXT { /** * data section */ OVERLAPPED m_olOverLapped; /**< Windows overlapped structure */ SOCKET m_sAssociatedSocket; /**< context associated socket */ WSABUF m_wsaBuffer; /**< the buffer to recieve WSASocket data */ CHAR m_cBuffer[MAX_BUFFER_SIZE]; /**< message buffer */ enum class Flag : unsigned char { Read, /**< read( recv ) */ Write, /**< write( send ) */ Accept /**< accept socket( for AcceptEx API ) */ } m_bFlag; /**< rw flag */ /** * operation section */ ...}using PIO_CONTEXT = IO_CONTEXT*;注意到完成鍵可以傳入某結構體或類(lèi)的地址,因此可以設計這樣一個(gè)結構體充當完成鍵傳遞給線(xiàn)程池中線(xiàn)程:
using HANDLE_CONTEXT = struct _HANDLE_CONTEXT { /** * data section */ SOCKET m_hClientSocket; /**< socket in thread to handle */ SOCKADDR_IN m_sClientAddr; /**< sockaddr_in in thread to handle */ std::vector<PIO_CONTEXT> m_vIoContext; /**< vector of IoContext pointer */ bool m_bFinished; /**< is process finished */ /** * operation section */ ...}using PHANDLE_CONTEXT = HANDLE_CONTEXT*;結構定義和piggyXP大同小異,主要差別就在于HANDLE_CONTEXT::m_bFinished項,在PostQueuedCompletionStatus傳遞時(shí)將其置為true,讓線(xiàn)程池中線(xiàn)程退出即可。
上下文大致運行流程如圖2所示,聰明的你一定一下就明白,就不贅述了??梢詤⒖?a rel="nofollow" target="_self">上一節所述流程,也可以參照代碼理解:
本節參考文獻
ISO. IEC14882:2011 Information technology – Programming languages – C++ [S]. Geneva, Switzerland: International Organization for Standardization, 2011.
Meyers S. Effective modern C++: 42 specific ways to improve your use of C++ 11 and C++ 14[M]. ” O’Reilly Media, Inc.”, 2014.
提示
這一節內容和本文主體關(guān)系不大,內容也不深,對本節不感興趣的朋友可以跳過(guò)。
例如piggyXP給出了如下的函數樣式的宏:
// 釋放指針宏#define RELEASE(x) {if(x != NULL ){delete x;x=NULL;}}而筆者在定義時(shí)選擇了內聯(lián)函數:
/*** Release memory*/template<typename _T>inline void ReleaseMemory( _T*& pMemory ) { if ( pMemory != nullptr ) { delete pMemory; pMemory = nullptr; }}主要代碼是差不多的,但是能夠完成的操作是不一樣的,聰明的你應該可以看出來(lái)。這個(gè)例子不一定好,那就再舉一個(gè)常見(jiàn)的:
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) // oopsint result_oops = MAX(i++, j); 選用function-like macro的好處只有一條:簡(jiǎn)單方便,效率高(空間換時(shí)間),缺點(diǎn)就不多說(shuō)了,看著(zhù)就明白。選用inline function最主要的好處就是:類(lèi)型檢查,效率高(可能空間換時(shí)間)。
在編程過(guò)程中請盡可能減少預處理器的使用(尤其是函數樣式的宏)。
我們可能習慣于這樣定義“常量”:
#define MAX_BUFFER_SIZE 8192當然,這是一個(gè)宏,在使用的時(shí)候替換為8192這一個(gè)字面量??紤]這樣的代碼:
#define N 2 + 3// oopsint oops = N / 2; // 3當然你也可以這樣定義,不過(guò)總覺(jué)得這樣定義很別扭:
#define N ( 2 + 3 )結果不用多說(shuō)。采用宏常量的理由還是:方便、效率高(字面值,在代碼中成為立即數),但是沒(méi)有類(lèi)型檢查(預處理器管理),有時(shí)候用著(zhù)很麻煩。
而以往的常量const又占用了存儲空間,而且畢竟存儲在內存中,也是可以變化的??紤]以下代碼:
const int constant = 0;int* evil_ptr = ( int* )&constant;*evil_ptr = 1;...這樣,一個(gè)常量就變化了。
更深入的討論高級程序員參考
事實(shí)上筆者在測試的時(shí)候發(fā)現如果對constant進(jìn)行輸出,會(huì )得到結果為0。反匯編后發(fā)現VS直接給輸出函數賦的是0,沒(méi)有從地址取值,優(yōu)化的還是可以。
在C++11中引入了常量表達式constexpr的概念,它是一個(gè)編譯期的常量(字面量),由編譯器負責執行。這樣,又可以進(jìn)行類(lèi)型檢查,又可以提高效率,減少資源占用,好處還是很多的。其中一個(gè):
constexpr std::size_t N = 2 + 3;// no oopsauto normal = N / 2; // 2聯(lián)系客服