I/O 完成連接埠( Windows核心編程 )

來源:互聯網
上載者:User

  一個服務應用程式的結構可以有兩種方式:

  •   在串列模式下,單個線程等待一個客戶發出請求(通常是通過網路)。當來了請求後,線程醒來處理客戶的請求。
  •   在並行存取模型下,單個線程等待客戶發出請求,而後建立新線程來處理請求。當新線程處理客戶請求時,起初的線程迴圈回去等待另一個客戶請求。處理客戶請求的線程處理完畢後終結。

 

  串列模型的問題在於它不能很好地處理好多個同時的請求,只適用於最簡單的服務程式。Ping伺服器是串列伺服器的一個很好的例子。

 

  因此並行存取模型就是最普通的了。它為每個請求都建立了一個新線程。而且通過增加硬體能力,會很容易使它的效能提高。

 

  當並行存取模型實現在 NT 上時,微軟 NT 小組注意到這些應用程式的效能沒有預料得那麼高。特別是有很多線程運行著的時候。因為所有這些線程都是可啟動並執行(沒有被掛起或等待什麼事),微軟意識到NT核心花了太多的時間來轉換運行線程的上下文(context),而真正留給線程來做它們自己的工作的時間卻被壓縮了。

 

  // 這個情況可以以我的一個例子來說明,我曾經花了一個下午去兵馬俑,結果來去花在路上的時間有4個小時,而在兵馬俑只呆了40分鐘。這個例子有點誇張,不過誇張有助於理解

 

  要使NT成為一個強大的伺服器環境,微軟就需要解決這個問題。解決的方法是一個稱為I/O完成連接埠的核心對象,它首次在NT3.5中被引入。I/O完成連接埠的理論基礎是並行啟動並執行線程的數目必須有一個上限。500個同時的客戶請求,並不意味著500個啟動並執行線程。但並發啟動並執行合適的線程數是多少呢?只要可啟動並執行線程數多於CPU數,作業系統一定要花時間來進行線程內容相關的切換的。

 

  並行模型的一個低效之處是為每一個客戶請求建立了一個新線程。建立線程比起建立進程來開銷要小,但也遠不是沒有開銷。如果當應用程式初始化時建立了一個線程池,而這些線程在應用程式執行期間是閒置,程式的效能就能進一步提高。I/O完成連接埠就使用線程池。
  I/O完成連接埠可能是Win32提供的最複雜的核心對象。要建立I/O完成連接埠,應調用 CreateIoCompletionPort:

 

  HANDLE CreateIoCompletionPort(HANDLE hFileHandle, HANDLE hExistingCompletionPort, DWORD dwCompletionKey, DWORD dwNumberOfConcurrentThreads);

 

  前三個參數只在把完成連接埠同裝置相關聯的時候才有用。如果不關聯裝置,只建立完成連接埠,那麼前三個參數可以為:INVALID_HANDLE_VALUE,NULL,0。最後一個參數指示I/O完成連接埠同時能啟動並執行最多線程數。如果為0,那麼預設為機器上的CPU數。不過你可以用幾個不同的值做實驗來確定哪個值有最佳的效能。順便說一句,這個函數是唯一一個建立了核心對象,而沒有 LPSECURITY_ATTRIBUTES 參數的 Win32 函數。這是因為完成連接埠只應用於一個進程內。

  當你建立一個I/O完成連接埠時,核心實際上建立了5個不同的資料結構。

 

  第一個是裝置列表。所有與完成連接埠相關聯的裝置都會出現在這個列表裡,結構就是:

 

hDevice dwCompletionKey

  當調用 CreateIoCompletionPort 關聯裝置時,表項就增加;當裝置控制代碼被關閉時,表項被刪除。

  裝置可以是:一個檔案,socket,郵件槽或管道等等。完成鍵可以自訂。

 

  第二個資料結構是一個I/O完成隊列。當一個裝置的非同步I/O請求完成時,系統檢查該裝置是否關聯了一個完成連接埠。如果是,系統就向該完成連接埠的I/O完成隊列裡加入完成的I/O請求項。該隊列中的每條表項給出了傳輸的位元組數,32位完成鍵,I/O請求的OVERLAPPED結構的指標和一個錯誤碼。

dwBytesTransferred dwCompletionKey pOverlapped dwError

  當I/O請求完成時或當PostQueuedCompletionStatus被調用時,表項被增加;當“等待線程隊列”中刪除一條表項時,表項被刪除。

  當服務應用程式初始化時,它應該建立I/O完成連接埠,而後應該建立一個線程池來處理客戶請求。現在的問題在於池中應該有多少線程。這是一個很難回答的問題。一個標準的答案是將電腦上的CPU的數目乘以2。

  池中的所有線程應該執行同一個線程函數。一般說來,該線程函數執行一些初始化後進入一個迴圈,該迴圈在服務進程終止時才結束。在迴圈中,線程使自己睡眠來等待完成連接埠的裝置I/O請求的完成。這是通過 GetQueuedCompletionStatus 來實現的:

 

  BOOL GetQueuedCompletionStatus(HANDLE hCompletionPort, LPDWORD lpdwNumberOfBytesTransferred, LPDWORD lpdwCompletionKey, LPOVERLAPPED* lpOverlapped, DWORD dwMilliseconds);

 

  第一個參數指出線程要監視哪個完成連接埠。很多服務應用程式只使用一個I/O完成連接埠,所有的I/O請求完成通知都發給了該連接埠。簡單地說,GetQueuedCompletionStatus 使調用線程進入睡眠,直到指定的完成連接埠的I/O完成隊列中出現了一項或直到逾時。

 

  I/O完成連接埠的第三個資料結構是 等待的線程隊列

 

dwThreadId

  當線程池中的一個線程調用 GetQueuedCompletionStatus 時,調用線程的 ID 就被放入等待線程隊列中。這樣,I/O完成連接埠對象總是知道哪個線程正在等待處理完成的I/O請求。當完成隊列裡出現一項時,完成連接埠就喚醒等待線程隊列裡的一個線程,並把所有資訊通過參數傳過去。

  要注意如何處理 GetQueuedCompletionStatus 的返回:

  

代碼 1 DWORD dwNumberOfBytesTransferred, dwCompletionKey;
 2 LPOVERLAPPED lpOverlapped;
 3 .
 4 .
 5 .
 6 BOOL fOk=GetQueuedCompletionStatus(hIOCompPort, 
 7 &dwNumberOfBytesTransferred, &dwCompletionKey, &lpOverlapped, 1000);
 8 DWORD dwError=GetLastError();
 9 if(fOk)
10 {
11   成功
12 }
13 else
14 {
15     if(lpOverlapped!=NULL)
16     {
17         I/O 請求失敗,dwError 包含錯誤碼
18     }
19     else
20     {
21         if(dwError==WAIT_TIMEOUT)
22         {
23             逾時
24         }
25         else
26         {
27             錯誤調用 GetQueuedCompletionStatus, dwError 包含錯誤碼
28         }
29     }
30 }

 


 

  I/O 完成隊列裡的表項是按照先進先出(FIFO)方式刪除的。但是調用 GetQueuedCompletionStatus 的線程卻是按照後進先出(LIFO)方式被喚醒的。原因也是為了提高效能。比如,有4個線程等線上程隊列中。如果出現了一個I/O項,最後一個調用 GetQueuedCompletionStatus 的線程被喚醒來處理這一項。當處理完後,它再次調用 GetQueuedCompletionStatus 進入等待線程隊列。這時如果出現了另一個I/O完成項,同一線程將被喚醒來處理這一新項。只要I/O請求完成的足夠慢,使得一個線程能處理它們,系統就總是喚醒同一個線程,其它三個線程將繼續休眠。通過使用LIFO演算法,不被調度的線程的記憶體資源(如棧空間)可以被交換到磁碟上和從處理器的緩衝中清除。這意味著有多個線程等待一個完成連接埠也沒有什麼壞處。

  現在該討論為什麼I/O完成連接埠這麼有用了。首先,當你建立一個I/O完成連接埠時,你指定了能並發啟動並執行線程的數目。前面說過,通常應該把該值設為電腦上CPU的數目。當完成的I/O項進入隊列時,I/O完成連接埠就要喚醒等待的線程。不過,完成連接埠只喚醒你指定的數目的線程。所以,如果有2個I/O請求完成了,而且有2個線程等在對 GetQueuedCompletionStatus 的調用上,I/O完成連接埠只喚醒1個線程,另1個線程將繼續休眠。當一個線程處理完一項後,它再次調用GetQueuedCompletionStatus,看到還有表項要處理,就喚醒同一線程來處理剩下的表項。

 

  如果認真想一下,就會發現這裡有問題:如果完成連接埠只能允許指定數目的線程並發地醒來,那麼線程池中為什麼要有多餘的線程等待呢?

  I/O完成連接埠是非常智能的。當完成連接埠喚醒一個線程時,它把線程的ID放在了同它相關聯的第四個資料結構——一個釋放線程列表中:

dwThreadId
 

  這使得完成連接埠能記住它喚醒了哪個線程並允許它監視這些線程的執行。如果一個釋放線程調用了某個函數使自己進入等待狀態,完成連接埠檢測到這一情況,就更新它的內部資料結構,把線程的ID從釋放線程列表移到暫停線程列表(I/O完成連接埠的最後一個資料結構):

dwThreadId
 

  完成連接埠的目標是使在釋放線程列表中的線程數與它被建立時指定的並發線程數相同。如果一個釋放線程因某種原因進入了等待狀態,釋放線程列表變小,完成連接埠就釋放另一個等待的線程。如果一個暫停線程醒來,它就離開暫停線程列表,重新進入釋放線程列表。這就意味著釋放線程列表中的線程數可能比允許的最大並發線程數要大。

  現在讓我們把這些合在一起。假設運行在一台雙CPU的電腦上。我們建立了一個完成連接埠允許最多2個線程並發醒來,又建立了4個線程等待完成的I/O請求。如果連接埠隊列中有3個完成的I/O請求,只有2個線程醒來處理這些請求。這減少了可運行線程的數目,節省了環境切換的時間。現在,如果第一個運行線程調用了 Sleep,WaitforSingleObject 等使它不能啟動並執行函數,I/O完成連接埠檢測到這一點,就立刻喚醒第3個線程。

  最終,第一個線程會再次運行。這使得運行線程數目大於系統中的CPU數。不過,完成連接埠會再次意識到這一點,線上程數目不超過CPU數之前,不會再喚醒其它線程。假定運行線程數超過最大值的時間會很短,當線程再次迴圈調用 GetQueuedCompletionStatus 時,數目會降下來。這就說明了為什麼線程池中的線程數要比完成連接埠的並發線程數設定要多。

現在該討論區對話池中應該有多少線程。首先,當服務應用程式初始化時,你要建立一組最小數目的線程,這樣就不必在運行時建立和釋放線程了。要記住,建立和釋放線程是浪費CPU時間的,所以最好減少這類事情發生。其次,你還要設定線程的最大數目,因為建立太多的線程會浪費系統資源。

  你可能要用不同的線程數目做實驗。IIS 伺服器使用了一個相當複雜的演算法來管理它的線程池。IIS 建立的最大線程數目是動態。當IIS初始化時,對每個CPU,它至多允許建立10個線程。不過,根據客戶請求,這一最大值可能還會增加。IIS 設的最大值是電腦上的記憶體數量的MB數的2倍。(* Jeffrey 詢問過IIS小組他們如何得到這一最大值的公式,被告知是感覺正確。你也應該為你的應用程式找到一個“感覺正確”的公式。)

  剛才,我們討論的是增加池中能有的最大線程數目。當數目改變時,新線程不會立刻被加到池裡。如果一個客戶請求到達時,池中所有線程都在忙,才會建立一個新線程。(假設現有的線程數小於現在的最大值)IIS 通過一個計數器來知道有多少線程忙。在調用 GetQueuedCompletionStatus 之前,計數器增加;在 GetQueuedCompletionStatus 返回之後,計數器減小。(* 你可以使用 InterlockedIncrement 和 InterlockedDecrement 函數來實現這一點)

要記住的很重要的一件事是,你應該使池中至少有一個線程能接受到來的客戶請求。

 

類比完成I/O請求

  BOOL PostQueuedCompletionStatus(HANDLE hCompletionPort, DWORD dwNumberOfBytesTransferred, DWORD dwCompletionKey, LPOVERLAPPED lpOverlapped);

  該函數允許你人工地向一個完成連接埠的I/O完成隊列裡加入一個完成I/O請求。這是非常有用的,它使你能同池中的所有線程通訊。例如,如果使用者想要終止一個服務應用程式,你就要讓所有的線程乾淨地退出。但如果線程等在完成連接埠上,而又沒有I/O請求到來,線程就不會醒來。通過對池中的每個線程調用一次 PostQueuedCompletionStatus ,線程就會醒來查看 GetQueuedCompletionStatus 的傳回值,發現應用程式正在終止,就能正確地清除和結束。

  在使用這一技術時必須小心。上面的例子行得通是因為池中的所有線程都在終止,不會再次調用 GetQueuedCompletionStatus 。不過,如果你想要通知線程某件事後,讓它們再迴圈回去調用 GetQueuedCompletionStatus,就可能會有問題。這是因為線程是按LIFO順序被喚醒的。所以你必須在應用程式中使用一些額外的線程同步技術來確保每個線程都有機會看到類比的 I/O 表項。否則,一個線程可能會見到幾次同樣的通知。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.