CreateIoCompletionPort和完成埠

ForTechnology發表於2011-08-11
摘自《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可以釋放完成埠的控制程式碼。
 

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/25897606/viewspace-704672/,如需轉載,請註明出處,否則將追究法律責任。

相關文章