本文僅是對Windows 在線文檔(部分)的翻譯
I/O完成端口為多處理器系統(tǒng)上的異步I/O請求提供了一個高效的線程模式。當進程創(chuàng)建一個I/O完成端口時,系統(tǒng)會創(chuàng)建相關(guān)的一系列隊列。結(jié)合一個預(yù)先初始化的線程池,進程通過使用完成端口可以更快,更高效地處理大量并發(fā)的異步I/O請求。
1 I/O完成端口工作流程
CreateIoCompletionPort函數(shù)用于創(chuàng)建I/O完成端口對象并將一個或多個文件句柄關(guān)聯(lián)到該端口上。當這些文件句柄上的異步I/O操作完成時,會以先入先出的順序向該完成端口的I/O完成隊列壓入一個完成封包。這個機制的強大之處在于將多個文件句柄的同步點整合到一個對象上。當然,這個機制也有其它用武之地。注意,雖然完成封包以先入先出的順序入隊,但可能以其它順序出隊。
術(shù)語file handle指一個抽象的重疊I/O設(shè)備,而不僅僅是磁盤上的文件。例如,可能是一個網(wǎng)絡(luò)設(shè)備,TCP套接字,命名管道,或郵件槽。其可以是任何支持重疊I/O的系統(tǒng)對象。
當某文件句柄和完成端口關(guān)聯(lián)后,除非該文件句柄上的完成封包從完成端口移除或原始操作同步地返回了錯誤,否則文件句柄的狀態(tài)不會更新。線程(由主線程創(chuàng)建的其它線程或主線程自己)使用GetQueuedCompletionStatus函數(shù)等待壓入到完成端口隊列的完成封包,而不是直接等待異步I/O完成。在完成端口上阻塞的線程將以后入先出的順序喚醒,而下一個完成封包將會以先入先出的順序從完成端口的I/O完成隊列中拉取。也就是說,當將完成封包分配給線程處理時,系統(tǒng)會喚醒最近與該完成端口關(guān)聯(lián)的線程。
在指定的完成端口上不限定調(diào)用GetQueuedCompletionStatus的線程數(shù)。當線程第一次調(diào)用GetQueuedCompletionStatus時,他將和指定的完成端口關(guān)聯(lián)(等待線程隊列),一直到以下情況發(fā)生:
- 線程退出
- 后續(xù)調(diào)用
GetQueuedCompletionStatus時指定另外的完成端口 - 關(guān)閉了完成端口
換句話說,一個線程在同一時刻最多只能關(guān)聯(lián)一個完成端口。
當有完成封包到達時,系統(tǒng)首先檢查當前有多少與完成端口關(guān)聯(lián)的線程正在運行。如果正在運行的線程數(shù)小于指定的最大并發(fā)數(shù),會喚醒其中一個線程(最近的那個)以處理完成封包。當運行的線程完成其處理時,通常會再次調(diào)用GetQueuedCompletionStatus,這時候,如果完成端口的I/O完成隊列不為空,它將繼續(xù)處理下一個完成封包,否則線程阻塞,等待完成封包。
線程可以調(diào)用PostQueuedCompltionStatus函數(shù)向完成端口的I/O完成隊列添加特殊的完成封包。這樣完成端口還可被用來從進程的其它線程接收自定義消息。這通常用于告知工作線程某些外部事件,如應(yīng)用程序即將終止運行。
I/O完成端口句柄以及與之關(guān)聯(lián)的文件句柄一起稱之為對完成端口的引用。只有當沒有引用時,完成端口對象才能被釋放。因此,為了釋放完成端口對象及相關(guān)資源,所有這些句柄必須被正確地關(guān)閉。在所有這些條件滿足后,應(yīng)用程序必須調(diào)用CloseHandle關(guān)閉完成端口句柄。
I/O完成端口和創(chuàng)建它的進程關(guān)聯(lián),無法在進程間共享,但是可以被進程內(nèi)的線程共享。
2 線程和并發(fā)
完成端口最重要的屬性是最大并發(fā)數(shù)。完成端口的最大并發(fā)數(shù)在其通過CreateIoCompletionPort創(chuàng)建時由NumberOfConcurrentThreads參數(shù)指定。這個參數(shù)限制與完成端口關(guān)聯(lián)的線程的可運行數(shù)。如果同完成端口關(guān)聯(lián)的正在運行的線程數(shù)已達到指定的最大并發(fā)值,系統(tǒng)將阻止其它關(guān)聯(lián)線程被喚醒,直到正在運行的線程數(shù)小于最大并發(fā)數(shù)。
最高效的情形是,當隊列中有完成封包等待時,由于完成端口上正在運行的線程數(shù)已達到其最大并發(fā)數(shù)而不會喚醒任何其它線程。在這種情況下,如果完成端口的隊列中總是有正在等待的完成封包,當正在運行的線程處理完上一個封包,然后調(diào)用GetQueuedCompletionStatus時,其不會阻塞而是立即獲得下一個完成封包并處理。這時沒有線程上下文切換,因為運行中的線程是連續(xù)地獲得完成封包的,同時其它線程仍然不能運行。
在上面的例子中,額外的線程似乎沒什么用,因為它們從來不運行。但是上面的情況是假設(shè)運行線程從來不會因為其它機制而進入等待狀態(tài)。
顯然,合適的最大并發(fā)數(shù)是機器的CPU數(shù)。如果線程處理的事務(wù)需要長時間運算,更大的并發(fā)數(shù)將允許更多線程得以運行。有些完成封包可能需要較長的時間進行處理,但多數(shù)完成封包的處理時間是差不多的??梢酝ㄟ^數(shù)值試驗獲取最佳的最大并發(fā)數(shù)。
如果與完成端口關(guān)聯(lián)的正在運行的線程因為其它原因進入等待狀態(tài),例如,調(diào)用了SuspendThread函數(shù),系統(tǒng)會允許因調(diào)用GetQueuedCompletionStatus而等待運行的線程處理完成封包。當之前進入等待的線程又開始運行時,可能有一個短暫的時間實際運行的線程數(shù)大于最大并發(fā)數(shù)。但是系統(tǒng)會通過禁止喚醒其它等待線程而快速減小這個實際并發(fā)數(shù)。這就是為什么應(yīng)用程序要將線程池線程數(shù)設(shè)置的比完成端口最大并發(fā)數(shù)大的原因。
3 支持函數(shù)
下面的函數(shù)可用于開始使用I/O完成端口的I/O操作。必須向函數(shù)傳遞OVERLAPPED結(jié)構(gòu)體實例且在此之前必須將相關(guān)的文件句柄和完成端口關(guān)聯(lián):
- ConnectNamedPipe
- DeviceIoControl
- LockFileEx
- ReadDirectoryChangesW
- ReadFile
- TransactNamedPipe
- WaitCommEvent
- WriteFile
- WSASendMsg
- WSASendTo
- WSARecvFrom
- WSARecvMsg
- WSARecv
4 APIs
CreateIoCompletionPort
創(chuàng)建一個I/O完成端口并將其和指定的文件句柄關(guān)聯(lián),或僅僅是創(chuàng)建一個完成端口。
在I/O完成端口上關(guān)聯(lián)一個打開的文件句柄將允許進程接收該文件句柄上的異步I/O操作的完成通知。
這里文件句柄是一個系統(tǒng)抽象的名詞,其代表一個重疊I/O端而不是磁盤上的一個文件。任何支持重疊I/O的系統(tǒng)對象,如網(wǎng)絡(luò)端點、TCP socket、命名管道或mail slots都可當做文件句柄。
Syntax
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle,
_In_opt_ HANDLE ExistingCompletionPort,
_In_ ULONG_PTR CompletionKey,
_In_ DWORD NumberOfConcurrentThreads
);
Parameters
FileHandle
一個打開的文件句柄或者是INVALID_HANDLE_VALUE。
該句柄必須是支持重疊I/O的對象。
如果指定了文件句柄,其必須以重疊I/O模式打開。例如,必須以FILE_FLAG_OVERLAPPED標識調(diào)用CreateFile函數(shù)以獲得一個文件句柄。
如果指定了INVALID_HANDLE_VALUE,函數(shù)將只是創(chuàng)建一個新的完成端口,這種情況下,ExistingCompletionPort必須是NULL且CompletionKey會被忽略。
ExistingCompletionPort
一個已存在的I/O完成端口句柄或者是NULL。
如果指定了一個已存在的完成端口,函數(shù)會將其和參數(shù)FileHandle指定的文件句柄關(guān)聯(lián)。如果函數(shù)執(zhí)行成功返回該完成端口。
如果該參數(shù)為NULL,函數(shù)將創(chuàng)建一個新的I/O完成端口。如果指定了有效的文件句柄(FileHandle),則新創(chuàng)建的完成端口會和其關(guān)聯(lián),否則只是新建一個完成端口。函數(shù)返回該新建的完成端口。
CompletionKey
包含于每個I/O完成封包中的用戶自定義的pre-handle。
NumberOfConcurrentThreads
對每個I/O完成端口,操作系統(tǒng)允許的最大線程數(shù)以同時處理I/O完成封包。如果ExistingCompletionPort參數(shù)不為NULL,該參數(shù)會被忽略。
如果該參數(shù)為0,系統(tǒng)將允許和系統(tǒng)處理器個數(shù)一樣的線程數(shù)同時運行。
Return value
函數(shù)執(zhí)行成功必然返回一個I/O完成端口。
- 如果ExistingCompletionPort為
NULL,返回一個新的完成端口。 - 如果ExistingCompletionPort為一個有效的完成端口,則返回這個完成端口。
- 如果FileHandle是一個有效的文件句柄,該文件句柄將會和返回的完成端口關(guān)聯(lián)。
- 如果函數(shù)失敗,返回
NULL,可通過GetLastError函數(shù)獲取擴展錯誤碼。
Remarks
I/O完成端口和創(chuàng)建它的進程關(guān)聯(lián),其它進程不可見,但是同一進程內(nèi)的線程之間可共享。
文件句柄僅可和一個完成端口關(guān)聯(lián),一旦完成關(guān)聯(lián),直到該文件句柄關(guān)閉該關(guān)聯(lián)將一直維持。
可多次調(diào)用CreateIoCompletionPort函數(shù)將多個文件句柄關(guān)聯(lián)到一個完成端口上。
使用CompletionKey參數(shù)幫助應(yīng)用程序跟蹤究竟是哪個重疊IO已經(jīng)完成。這個參數(shù)并沒有參與CreateIoCompletionPort的內(nèi)部功能控制,其只是綁定到文件句柄上。對于每個文件句柄來說這個CompletionKey必須是唯一的,且其在整個內(nèi)部處理期間都會伴隨文件句柄。當完成封包到來時,可通過GetQueuedCompletionStatus函數(shù)獲取這個CompletionKey。CompletionKey也可用于PostQueuedCompletionStatus函數(shù)以入隊用戶自定義完成封包。
當文件句柄和某IO完成端口關(guān)聯(lián)后,其不可再用于ReadFileEx函數(shù)和WriteFileEx函數(shù),因為這些函數(shù)有它們自己的異步IO機制。
最好不要以句柄繼承或調(diào)用DuplicateHandle函數(shù)的方式共享已經(jīng)與IO完成端口關(guān)聯(lián)的文件句柄。使用這種多重句柄執(zhí)行操作時也會產(chǎn)生完成通知,還是小心為妙。
IO完成端口句柄和與其關(guān)聯(lián)的文件句柄我們稱之為IO完成端口引用(reference to the I/O completion port),當沒有引用時必須釋放IO完成端口。所有這些句柄必須正確地關(guān)閉以釋放IO完成端口及其關(guān)聯(lián)的系統(tǒng)資源。當滿足上述條件時,可調(diào)用CloseHandle關(guān)閉IO完成端口。
GetQueuedCompletionStatus
嘗試從指定的IO完成端口上出隊一個IO完成封包。如果隊列中沒有完成封包,函數(shù)將等待完成端口上某個未決IO操作完成。
如果需要一次出隊多個IO完成封包,使用GetQueuedCompletionStatusEx函數(shù)。
Syntax
BOOL WINAPI GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_Out_ LPDWORD lpNumberOfBytes,
_Out_ PULONG_PTR lpCompletionKey,
_Out_ LPOVERLAPPED *lpOverlapped,
_In_ DWORD dwMilliseconds
);
Parameters
CompletionPort
lpNumberOfBytes
用于保存已完成的IO操作在其執(zhí)行期間傳輸?shù)淖止?jié)總數(shù)。
lpCompletonKey
當某文件句柄上的IO操作完成時,用于保存與該文件句柄關(guān)聯(lián)的completion key的值。
lpOverlapped
用于保存某OVERLAPPED結(jié)構(gòu)體的地址,其在IO操作開始時被指定。
即使已將文件句柄和完成端口關(guān)聯(lián)且指定了有效的OVERLAPPED結(jié)構(gòu),應(yīng)用程序也可以阻止完成通知。為此,必須在OVERLAPPED結(jié)構(gòu)的hEvent成員中保存一個有效的事件句柄并設(shè)置其最低位:
Overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
Overlapped.hEvent = (HANDLE) ((DWORD_PTR) Overlapped.hEvent | 1);
...
另外,在關(guān)閉這個事件句柄時不要忘了將最低位清掉:
CloseHandle((HANDLE) ((DWORD_PTR) Overlapped.hEvent & ~1));
dwMilliseconds
調(diào)用者愿意在完成端口上等待完成封包的毫秒數(shù)。如果在指定的時間內(nèi)沒有完成封包出現(xiàn),函數(shù)將返回FALSE同時設(shè)置lpOverlapped為NULL。
如果為INFINITE,函數(shù)將阻塞執(zhí)行。
Return value
這個函數(shù)將線程和指定的完成端口關(guān)聯(lián)。一個線程只能和一個完成端口關(guān)聯(lián)。
當調(diào)用GetQueuedCompletionStatus時如果與之關(guān)聯(lián)的完成端口已經(jīng)關(guān)閉,函數(shù)將失敗并返回FALSE,且lpOverlapped為NULL,同時GetLastError返回ERROR_ABANDONED_WAIT_0擴展錯誤碼。
如果GetQueuedCompletionStatus函數(shù)執(zhí)行成功,將從完成端口上出隊一個完成封包(對應(yīng)一個成功的IO操作),并將其信息存儲到lpNumberOfBytes, lpCompletionKey和lpOverlapped參數(shù)中;如果執(zhí)行失敗,這些參數(shù)可能包含以下特定的值:
- 如果lpOverlapped為
NULL,函數(shù)沒有從完成端口上出隊一個完成封包,這種情況下,函數(shù)不會在lpNumberOfBytes, lpCompletionKey中存儲信息,它們的值是不確定的。 - 如果lpOverlapped不為
NULL,函數(shù)從完成端口上出隊一個完成封包,但該完成封包對應(yīng)一個失敗的IO操作,函數(shù)會將該失敗的IO操作的信息存儲到lpNumberOfBytes, lpCompletionKey和lpOverlapped中,可通過GetLastError獲取擴展錯誤碼。
PostQueuedCompletionStatus
向完成端口提交一個IO完成封包。
Syntax
BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort,
_In_ DWORD dwNubmerOfBytesTransferred,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);
Parameters
CompletionPort
dwNumberOfBytesTransferred
dwCompletonKey
lpOverlapped
上述3個參數(shù)指定當調(diào)用GetQueuedCompletionStatus時對應(yīng)參數(shù)帶回的值。
Return value
函數(shù)執(zhí)行成功返回非零值,否則返回0.
Remarks
提交的完成封包完全滿足GetQueuedCompletonStatus的要求。系統(tǒng)不會使用這個封包也不會驗證其正確性。尤其是,lpOverlapped參數(shù)不必是指向OVERLAPPED結(jié)構(gòu)體的指針。