.NET非同步程式設計:IO完成埠與BeginRead

monicamh發表於2011-03-24

    【IT168 專稿】寫這個系列原本的想法是討論一下.NET中非同步程式設計風格的變化,特別是F#中的非同步工作流以及未來的.NET 5.0中的基於任務的非同步程式設計模型。但經過前幾篇文章(為什麼需要非同步傳統的非同步程式設計使用CPS及yield實現非同步)的發表後,很多人對IO非同步背後實現的原理以及為什麼這樣能提高效能很感興趣。其實我本不想花更多的文字在這些底層實現的細節上,一來我並不擅長這些方面,二來我們使用.NET的非同步IO就不需要關心這些底層東西,因為已經為你封裝完備了。不過為了避免大家一再在這上面商討,我還是在這個系列中間插入了一篇來解釋一下。

  本文我將從核心物件IO完成埠開始介紹,然後來瞧瞧.NET BCL中的FileStream.BeginRead是如何利用IO完成埠來實現的。

  IO完成埠(IO Completion Port)

  大多數人應該或多或少地聽說過IO完成埠這麼個東西,而且也知道它是實現高效能IO,高伸縮性應用的尚方寶劍。IO完成埠是一個非常複雜的核心物件,其實現的也非常巧妙,細細琢磨還是非常有意思的。

  建立高伸縮性的應用的一個基本原則就是:建立更少的執行緒。執行緒數更少首先消耗的資源就少,每個執行緒的建立除了要浪費CPU時間外,還要建立一系列的資料結構用來儲存執行緒相關的一些資訊:使用者棧,執行緒上下文,核心棧等。這個總共加起來大概1.5M左右,那麼你算算你的32位機器總共能使用多少記憶體?那麼對應地能建立多少執行緒?可能有人講那對於64位的就無所謂了。嗯,在資源佔用這方面64位確實不用擔心。但是系統中可執行的執行緒數越多,你的CPU數又是有限的(8個?80個?)。Windows的任務排程機制是每個執行緒會執行一個時間片,然後Windows搶佔式的排程另一個執行緒執行。那麼執行緒數越多,Windows勢必要進行更頻繁的執行緒上下文切換。執行緒上下文切換對系統效能的影響在這裡我就不多說了,你可以搜搜資料。

  那麼如何做到建立更少的執行緒,而又幹更多的事兒呢?答案就是“不等待”。相對CPU來說,IO裝置的速度簡直低的要命。就好像飛機和拖拉機的差別一樣,我們可不能讓拖拉機拖了飛機的後退兒。而IO完成埠就是為了這個而生的:建立更少的執行緒,幹更多的事兒。

  IO完成埠首先不是一個我們看得見摸得著的什麼插口,也和我們常說的80這樣的埠不同。你可以將其理解為一個資料結構或一個物件(下面我會用C#的程式碼來輔助講解IO完成埠,僅僅是講解,這些程式碼並不是真實的實現):

  Windows提供了一個CreateIoCompletionPort API來建立IO完成埠,實際上這個API有兩個作用:建立IO完成埠和將一個IO裝置與該埠繫結。建立IO完成埠時有一個很重要的引數:指定同時最多能有多少個執行緒並行執行,這就是為了保證更少的執行緒,如果你將這個數值指定為0,那麼預設值就會是你機器的CPU數。IO埠裡還有一個IO裝置控制程式碼列表,你可以將很多裝置控制程式碼與這個埠繫結(檔案、Socket等):

#div_code img{border:0px;}<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--&gt//函式原型
HANDLE CreateIoCompletionPort(
    
//裝置控制程式碼
    HANDLE     hFile,
    
//已有的IO完成埠控制程式碼,如果這裡已經指定,則是將前面指定的裝置與該埠繫結
    HANDLE     hExistingCompletionPort,
    
//因為一個IO完成埠可以繫結很多裝置,可以用這個來區分
    ULONG_PTR  CompletionKey,
    
//允許同時執行的執行緒數
    DWORD      dwNumberOfConcurrentThreads
);
//建立一個IO完成埠
HANDLE hIoPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,2);
//建立檔案,如果要非同步訪問檔案則需要指定FILE_FLAG_OVERLAPPED
HANDLE hFile = CreateFile(..);
//將上面建立的檔案控制程式碼與剛才建立的IO完成埠繫結,不僅僅是檔案可以
CreateIoCompletionPort(hFile,hIoPort,1,2);

  除此之外,我們還要為該埠建立一些供使用的執行緒。然後讓這些執行緒呼叫Windows提供的GetQueuedCompletionStatus方法。這些執行緒呼叫了該方法後會被放到IO完成埠另外一個資料結構中:一個後進先出的佇列(我們將其稱為等待佇列吧)。然後該執行緒會休眠起來,不佔用CPU。然後我們可以呼叫像ReadFile這樣的方法發起一個IO請求:

#div_code img{border:0px;}<!--

Code highlighting produced by Actipro CodeHighlighter (freeware)
http://www.CodeHighlighter.com/

--&gtBOOL ReadFile(
    HANDLE hFile,
    PVOID  pvBuffer,
    DWORD  nNumBytesToRead,
    PDWORD pdwNumBytes,
    OVERLAPPED
* pOverlapped);

ReadFile(..
&overlapped);

相關文章