摘自《Networking Programming for Microsoft Windows》第八章
“完成連接埠”模型是迄今為止最為複雜的一種I/O模型。然而,假若一個應用程式同時需要管理為數眾多的通訊端,那麼採用這種模型,往往可以達到最佳的系統效能!
從本質上說,完成連接埠模型要求我們建立一個Win32完成連接埠對象,通過指定數量的線程,對重疊I/O請求進行管理,以便為已經完成的重疊I/O請求提供服務。
使用這種模型之前,首先要建立一個I/O完成連接埠對象,用它面向任意數量的通訊端控制代碼,管理多個I/O請求。要做到這一點,需要調用CreateCompletionPort函數。
該函數定義如下:
HANDLE CreateIoCompletionPort(
HANDLE FileHandle,
HANDLE ExistingCompletionPort,
ULONG_PTR CompletionKey,
DWORD NumberOfConcurrentThreads
);
在我們深入探討其中的各個參數之前,首先要注意該函數實際用於兩個明顯有別的目的:
1. 用於建立一個完成連接埠對象。
2. 將一個控制代碼同完成連接埠關聯到一起。
最開始建立一個完成連接埠時,唯一感興趣的參數便是NumberOfConcurrentThreads(並發線程的數量);前面三個參數都會被忽略。NumberOfConcurrentThreads參數的特殊之處在於,它定義了在一個完成連接埠上,同時允許執行的線程數量。理想情況下,我們希望每個處理器各自負責一個線程的運行,為完成連接埠提供服務,避免過於頻繁的線程“情境”切換。若將該參數設為0,表明系統內安裝了多少個處理器,便允許同時運行多少個線程!可用下述代碼建立一個I/O完成連接埠:
hIOCP = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
該語句的作用是返回一個控制代碼,在為完成連接埠分配了一個通訊端控制代碼後,用來對那個連接埠進行標定(引用)。
一、工作者線程與完成連接埠
成功建立一個完成連接埠後,便可開始將通訊端控制代碼與對象關聯到一起。但在關聯通訊端之前,首先必須建立一個或多個“工作者線程”,以便在I/O請求投遞給完成連接埠對象後,為完成連接埠提供服務。在這個時候,大家或許會覺得奇怪,到底應建立多少個線程,以便為完成連接埠提供服務呢?這實際正是完成連接埠模型顯得頗為“複雜”的一個方面,因為服務I/O請求所需的數量取決於應用程式的總體設計情況。在此要記住的一個重點在於,在我們調用CreateIoCompletionPort時指定的並發線程數量,與打算建立的工作者線程數量相比,它們代表的並非同一件事情。早些時候,我們曾建議大家用CreateIoCompletionPort函數為每個處理器
都指定一個線程(處理器的數量有多少,便指定多少線程)以避免由於頻繁的線程“情境”交換活動,從而影響系統的整體效能。CreateIoCompletionPort函數的NumberOfConcurrentThreads參數明確指示系統:在一個完成連接埠上,一次只允許n個工作者線程運行。假如在完成連接埠上建立的工作者線程數量超出n個,那麼在同一時刻,最多隻允許n個線程運行。但實際上,在一段較短的時間內,系統有可能超過這個值,但很快便會把它減少至事先在CreateIoCompletionPort函數中設定的值。那麼,為何實際建立的工作者線程數量有時要比CreateIoCompletionPort函數設定的多一些呢?這樣做有必要嗎?如先前所述,這主要取決於
應用程式的總體設計情況。假定我們的某個工作者線程調用了一個函數,比如Sleep或WaitForSingleObject,但卻進入了暫停(鎖定或掛起)狀態,那麼允許另一個線程代替它的位置。換言之,我們希望隨時都能執行儘可能多的線程;當然,最大的線程數量是事先在CreateIoCompletionPort調用裡設定好的。這樣一來,假如事先預計到自己的線程有可能暫時處於停頓狀態,那麼最好能夠建立比CreateIoCompletionPort的NumberOfConcurrentThreads參數的值多的線程,以便到時候充分發揮系統的潛力。一旦在完成連接埠上擁有足夠多的工作者線程來為I/O請求提供服務,便可著手將通訊端控制代碼同完成連接埠關聯到一起。這要求我們在一個現有的完成連接埠上,調用CreateIoCompletionPort函數,同時為前三個參數——FileHandle,ExistingCompletionPort和CompletionKey——提供通訊端的資訊。其中, FileHandle參數指定一個要同完成連接埠關聯在一起的通訊端控制代碼。ExistingCompletionPort參數指定的是一個現有的完成連接埠。CompletionKey(完成鍵)參數則指定要與某個特定通訊端控制代碼關聯在一起的“單控制代碼資料”;在這個參數中,應用程式可儲存與一個通訊端對應的任意類型的資訊。之所以把它叫作“單控制代碼資料”,是由於它只對
應著與那個通訊端控制代碼關聯在一起的資料。可將其作為指向一個資料結構的指標,來儲存通訊端控制代碼;在那個結構中,同時包含了通訊端的控制代碼,以及與那個通訊端有關的其他資訊。
根據我們到目前為止學到的東西,首先來構建一個基本的應用程式架構。下面闡述了如何使用完成連接埠模型,來開發一個ECHO伺服器應用。在這個程式中,我們基本上按下述步驟行事:
1) 建立一個完成連接埠。第四個參數保持為0,指定在完成連接埠上,每個處理器一次只允許執行一個工作者線程。
2) 判斷系統內到底安裝了多少個處理器。
3) 建立工作者線程,根據步驟2)得到的處理器資訊,在完成連接埠上,為已完成的I/O請求提供服務。
4) 準備好一個監聽通訊端,在連接埠5150上監聽進入的串連請求。
5) 使用accept函數,接受進入的串連請求。
6) 建立一個資料結構,用於容納“單控制代碼資料”,同時在結構中存入接受的通訊端控制代碼。
7) 調用CreateIoCompletionPort,將自accept返回的新通訊端控制代碼同完成連接埠關聯到一起。通過完成鍵(CompletionKey)參數,將單控制代碼資料結構傳遞給CreateIoCompletionPort。
8) 開始在已接受的串連上進行I/O操作。在此,我們希望通過重疊I/O機制,在建立的通訊端上投遞一個或多個非同步WSARecv或WSASend請求。這些I/O請求完成後,一個工作者線程會為I/O請求提供服務,同時繼續處理未來的I/O請求,稍後便會在步驟3 )指定的工作者常式中,體驗到這一點。
9) 重複步驟5 ) ~ 8 ),直至伺服器中止。
二、完成連接埠和重疊I/O
將通訊端控制代碼與一個完成連接埠關聯在一起後,便可以通訊端控制代碼為基礎,投遞發送與接收請求,開始對I/O請求的處理。接下來,可開始依賴完成連接埠,來接收有關I/O操作完成情況的通知。從本質上說,完成連接埠模型利用了Win32重疊I/O機制。在這種機制中,象WSASend和WSARecv這樣的Winsock API調用會立即返回。此時,需要由我們的應用程式負責在以後的某個時間,通過一個OVERLAPPED結構,來接收調用的結果。在完成連接埠模型中,要想做到這一點,需要使用GetQueuedCompletionStatus(擷取排隊完成狀態)函數,讓一個或多個工作者線程在完成連接埠上等待。該函數的定義如下:
BOOL GetQueuedCompletionStatus(
HANDLE CompletionPort,
LPDWORD lpNumberOfBytes,
PULONG_PTR lpCompletionKey,
LPOVERLAPPED* lpOverlapped,
DWORD dwMilliseconds
);
其中,CompletionPort參數對應於要在上面等待的完成連接埠。lpNumberOfBytes參數負責在完成了一次I/O操作後(如WSASend或WSARecv),接收實際傳輸的位元組數。lpCompletionKey參數為原先傳遞進入CreateIoCompletionPort函數的通訊端返回“單控制代碼資料”。如我們早先所述,大家最好將通訊端控制代碼儲存在這個“鍵”(Key)中。lpOverlapped參數用於接收完成的I/O操作的重疊結果。這實際是一個相當重要的參數,因為可用它擷取每個I/O操作的資料。而最後一個參數,dwMilliseconds,用於指定調用者希望等待一個完成資料包在完成連接埠上出現的時間。假如將其設為INFINITE,調用會無休止地等待下去。
三、單控制代碼資料和單I/O操作資料
一個工作者線程從GetQueuedCompletionStatus這個API調用接收到I/O完成通知後,在lpCompletionKey和lpOverlapped參數中,會包含一些必要的通訊端資訊。利用這些資訊,可通過完成連接埠,繼續在一個通訊端上的I/O處理。通過這些參數,可獲得兩方面重要的通訊端資料:單控制代碼資料,以及單I/O操作資料。其中,lpCompletionKey參數包含了“單控制代碼資料”,因為在一個通訊端首次與完成連接埠關聯到一起的時候,那些資料便與一個特定的通訊端控制代碼對應起來了。這些資料正是我們在進行CreateIoCompletionPort API調用的時候,通過CompletionKey參數傳遞的。如早先所述,應用程式可通過該參數傳遞任意類型的資料。通常情況下,應用程式會將與I/O請求有關的通訊端控制代碼儲存在這裡。lpOverlapped參數則包含了一個OVERLAPPED結構,在它後面跟隨“單I/O操作資料”。我們的工作者線程處理一個完成資料包時(將資料原封不動打轉回去,接受串連,投遞另一個線程,等等),這些資訊是它必須要知道的。單I/O操作資料可以是追加到一個OVERLAPPED結構末尾的、任意數量的位元組。假如一個函數要求用到一個OVERLAPPED結構,我們便必須將這樣的一個結構傳遞進去,以滿足它的要求。要想做到這一點,一個簡單的方法是定義一個結構,然後將OVERLAPPED結構作為新結構的第一個元素使用。舉個例子來說,可定義下述資料結構,實現對單I/O操作資料的管理:
typedef struct
{
OVERLAPPED Overlapped;
WSABUF DataBuf;
CHAR Buffer[DATA_BUFSIZE];
BOOL OperationType;
}PER_IO_OPERATION_DATA
該結構示範了通常要與I/O操作關聯在一起的某些重要資料元素,比如剛才完成的那個I/O操作的類型(發送或接收請求)。在這個結構中,我們認為用於已完成I/O操作的資料緩衝區是非常有用的。要想調用一個Winsock API函數,同時為其分配一個OVERLAPPED結構,既可將自己的結構“造型”為一個OVERLAPPED指標,亦可簡單地撤消對結構中的OVERLAPPED元素的引用。如下例所示:
PER_IO_OPERATION_DATA PerIoData;
// 可像下面這樣調用一個函數
WSARecv(socket, ..., (OVERLAPPED *)&PerIoData);
// 或像這樣
WSARecv(socket, ..., &(PerIoData.Overlapped));
在背景工作執行緒的後面部分,等GetQueuedCompletionStatus函數返回了一個重疊結構(和完成鍵)後,便可通過撤消對OperationType成員的引用,調查到底是哪個操作投遞到了這個控制代碼之上(只需將返回的重疊結構造型為自己的PER_IO_OPERATION_DATA結構)。對單I/O操作資料來說,它最大的一個優點便是允許我們在同一個控制代碼上,同時管理多個I/O操作(讀/寫,多個讀,多個寫,等等)。大家此時或許會產生這樣的疑問:在同一個通訊端上,真的有必要同時投遞多個I/O操作嗎?答案在於系統的“伸縮性”,或者說“擴充能力”。例如,假定我們的機器安裝了多個中央處理器,每個處理器都在運行一個工作者線程,那麼在同一個時
候,完全可能有幾個不同的處理器在同一個通訊端上,進行資料的收發操作。
最後要注意的一處細節是如何正確地關閉I/O完成連接埠—特別是同時運行了一個或多個線程,在幾個不同的通訊端上執行I/O操作的時候。要避免的一個重要問題是在進行重疊I/O操作的同時,強行釋放一個OVERLAPPED結構。要想避免出現這種情況,最好的辦法是針對每個通訊端控制代碼,調用closesocket函數,任何尚未進行的重疊I/O操作都會完成。一旦所有通訊端控制代碼都已關閉,便需在完成連接埠上,終止所有工作者線程的運行。要想做到這一點, 需要使用PostQueuedCompletionStatus函數,向每個工作者線程都發送一個特殊的完成資料包。該函數會指示每個線程都“立即結束並退出”。下面是PostQueuedCompletionStatus函數的定義:
BOOL PostQueuedCompletionStatus(
HANDLE CompletionPort,
DWORD dwNumberOfBytesTransferred,
ULONG_PTR dwCompletionKey,
LPOVERLAPPED lpOverlapped
);
其中,CompletionPort參數指定想向其發送一個完成資料包的完成連接埠對象。而就dwNumberOfBytesTransferred、dwCompletionKey和lpOverlapped這三個參數來說,每一個都允許我們指定一個值,直接傳遞給GetQueuedCompletionStatus函數中對應的參數。這樣一來,一個工作者線程收到傳遞過來的三個GetQueuedCompletionStatus函數參數後,便可根據由這三個參數的某一個設定的特殊值,決定何時應該退出。例如,可用dwCompletionPort參數傳遞0值,而一個工作者線程會將其解釋成中止指令。一旦所有工作者線程都已關閉,便可使用CloseHandle函數,關閉完成連接埠,最終安全退出程式。
註:CreateIoCompletionPort ,PostQueuedCompletionStatus ,GetQueuedCompletionStatus 等函數的用法說明。
Platform SDK: Storage
I/O Completion Ports
I/O completion ports are the mechanism by which an application uses a pool of threads that was created when the application was started to process asynchronous I/O requests. These threads are created for the sole purpose of processing I/O requests. Applications that process many concurrent asynchronous I/O requests can do so more quickly and efficiently by using I/O completion ports than by using creating threads at the time of the I/O request.
I/O完成連接埠(s)是一種機制,通過這個機制,應用程式在啟動時會首先建立一個線程池,然後該應用程式使用線程池處理非同步I/O請求。這些線程被建立的唯一目的就是用於處理I/O請求。對於處理大量並發非同步I/O請求的應用程式來說,相比於在I/O請求發生時建立線程來說,使用完成連接埠(s)它就可以做的更快且更有效率。
The CreateIoCompletionPort function associates an I/O completion port with one or more file handles. When an asynchronous I/O operation started on a file handle associated with a completion port is completed, an I/O completion packet is queued to the port. This can be used to combine the synchronization point for multiple file handles into a single object.
CreateIoCompletionPort函數會使一個I/O完成連接埠與一個或多個檔案控制代碼發生關聯。當與一個完成連接埠相關的檔案控制代碼上啟動的非同步I/O操作完成時,一個I/O完成包就會進入到該完成連接埠的隊列中。對於多個檔案控制代碼來說,就可以把這些多個檔案控制代碼合并成一個單獨的對象,這個可以被用來結合約步點?
A thread uses the GetQueuedCompletionStatus function to wait for a completion packet to be queued to the completion port, rather than waiting directly for the asynchronous I/O to complete. Threads that block their execution on a completion port are released in last-in-first-out (LIFO) order. This means that when a completion packet is queued to the completion port, the system releases the last thread to block its execution on the port.
調用GetQueuedCompletionStatus函數,某個線程就會等待一個完成包進入到完成連接埠的隊列中,而不是直接等待非同步I/O請求完成。線程(們)就會阻塞於它們的運行在完成連接埠(按照後進先出隊列順序的被釋放)。這就意味著當一個完成包進入到完成連接埠的隊列中時,系統會釋放最近被阻塞在該完成連接埠的線程。
When a thread calls GetQueuedCompletionStatus, it is associated with the specified completion port until it exits, specifies a different completion port, or frees the completion port. A thread can be associated with at most one completion port.
調用GetQueuedCompletionStatus,線程就會將會與某個指定的完成連接埠建立聯絡,一直延續其該線程的存在周期,或被指定了不同的完成連接埠,或者釋放了與完成連接埠的聯絡。一個線程只能與最多不超過一個的完成連接埠發生聯絡。
The most important property of a completion port is the concurrency value. The concurrency value of a completion port is specified when the completion port is created. This value limits the number of runnable threads associated with the completion port. When the total number of runnable threads associated with the completion port reaches the concurrency value, the system blocks the execution of any subsequent threads that specify the completion port until the number of runnable threads associated with the completion port drops below the concurrency value. The most efficient scenario occurs when there are completion packets waiting in the queue, but no waits can be satisfied because the port has reached its concurrency limit. In this case, when a running thread calls GetQueuedCompletionStatus, it will immediately pick up the queued completion packet. No context switches will occur, because the running thread is continually picking up completion packets and the other threads are unable to run.
完成連接埠最重要的特性就是並發量。完成連接埠的並發量可以在建立該完成連接埠時指定。該並發量限制了與該完成連接埠相關聯的可運行線程的數目。當與該完成連接埠相關聯的可運行線程的總數目達到了該並發量,系統就會阻塞任何與該完成連接埠相關聯的後續線程的執行,直到與該完成連接埠相關聯的可運行線程數目下降到小於該並發量為止。最有效假想是發生在有完成包在隊列中等待,而沒有等待被滿足,因為此時完成連接埠達到了其並發量的極限。此時,一個正在運行中的線程調用GetQueuedCompletionStatus時,它就會立刻從隊列中取走該完成包。這樣就不存在著環境的切換,因為該處於運行中的線程就會連續不斷地從隊列中取走完成包,而其他的線程就不能運行了。
The best value to pick for the concurrency value is the number of CPUs on the machine. If your transaction required a lengthy computation, a larger concurrency value will allow more threads to run. Each transaction will take longer to complete, but more transactions will be processed at the same time. It is easy to experiment with the concurrency value to achieve the best effect for your application.
對於並發量最好的挑選值就是您電腦中cpu的數目。如果您的交易處理需要一個漫長的計算時間,一個比較大的並發量可以允許更多線程來運行。雖然完成每個交易處理需要花費更長的時間,但更多的事務可以同時被處理。對於應用程式來說,很容易通過測試並發量來獲得最好的效果。
The PostQueuedCompletionStatus function allows an application to queue its own special-purpose I/O completion packets to the completion port without starting an asynchronous I/O operation. This is useful for notifying worker threads of external events.
PostQueuedCompletionStatus函數允許應用程式可以針對自訂的專用I/O完成包進行排隊,而無需啟動一個非同步I/O操作。這點對於通知外來事件的工作者線程來說很有用。
The completion port is freed when there are no more references to it. The completion port handle and every file handle associated with the completion port reference the completion port. All the handles must be closed to free the completion port. To close the port handle, call the CloseHandle function.
在沒有更多的引用針對某個完成連接埠時,需要釋放該完成連接埠。該完成連接埠控制代碼以及與該完成連接埠相關聯的所有檔案控制代碼都需要被釋放。調用CloseHandle可以釋放完成連接埠的控制代碼。
轉自:http://blog.csdn.net/hionceshine/article/details/3362669