天天看點

IOCP

載自:http://blog.csdn.net/markman101/article/details/6235516

本文主要探讨一下windows平台上的完成端口開發及其與之相關的幾個重要的技術概念,這些概念都是與基于IOCP的開發密切相關的,對開發人員來講,又不得不給予足夠重視的幾個概念:

1) 基于IOCP實作的服務吞吐量

2)IOCP模式下的線程切換

3)基于IOCP實作的消息的亂序問題。 

一、IOCP簡介 

    提到IOCP,大家都非常熟悉,其基本的程式設計模式,我就不在這裡展開了。在這裡我主要是把IOCP中所提及的概念做一個基本性的總結。IOCP的基本架構圖如下:

IOCP

如圖所示:在IOCP中,主要有以下的參與者:

--》完成端口:是一個FIFO隊列,作業系統的IO子系統在IO操作完成後,會把相應的IO packet放入該隊列。

--》等待者線程隊列:通過調用GetQueuedCompletionStatus API,在完成端口上等待取下一個IO packet。

--》執行者線程組:已經從完成端口上獲得IO packet,在占用CPU進行處理。

除了以上三種類型的參與者。我們還應該注意兩個關聯關系,即:

--》IO Handle與完成端口相關聯:任何期望使用IOCP的方式來處理IO請求的,必須将相應的IO Handle與該完成端口相關聯。需要指出的時,這裡的IO Handle,可以是File的Handle,或者是Socket的Handle。

--》線程與完成端口相關聯:任何調用GetQueuedCompletionStatus API的線程,都将與該完成端口相關聯。在任何給定的時候,該線程隻能與一個完成端口相關聯,與最後一次調用的GetQueuedCompletionStatus為準。

二、高并發的伺服器(基于socket)實作方法 

        一般來講,實作基于socket的伺服器,有三種實作的方式(thread per request的方式,我就不提了:)):

第一、線程池的方式。 使用線程池來對用戶端請求進行服務。使用這種方式時,當用戶端對伺服器的連接配接 是短連接配接(所謂的短連接配接,即:用戶端對伺服器不是長時間連接配接)時,是可以考慮的。但是,如若用戶端對伺服器的連接配接是長連接配接時,我們需要限制伺服器端的最大 連接配接數目為線程池線程的最大數目,而這應用的設計本身來講,是不好的設計方式,scalability會存在問題。

第二、基于Select的伺服器實作 。其本質是,使用Select(作業系統提供的API)來監視 連接配接是否可讀,可寫,或者是否出錯。相比于前一種方式,Select允許應用使用一個線程(或者是有限幾個線程)來監視連接配接的可讀寫性。當有連接配接可讀可寫 時,應用可以以non-bolock的方式讀寫socket上的資料。使用Select的方式的缺點是,當Select所監視的連接配接數目在千的數量級時, 性能會打折扣。這是因為作業系統核心需要在内部對這些Socket進行輪詢,以檢查其可讀寫性。另一個問題是:應用必須在處理完所有的可讀寫socket 的IO請求之後,才能再次調用Select,進行下一輪的檢查,否則會有潛在的問題。這樣,造成的結果是,對一些請求的處理會出現饑餓的現象。

        一般common的做法是Select結合Leader-Follower設計模式使用。不過不管怎樣,Select的本質造成了其在Scalability的問題是不如IOCP,這也是很多high-scalabe的伺服器采用IOCP的原因。

第三、IOCP實作高并發的伺服器 。IOCP是實作high-scalabe的伺服器的首選。其特點我們專門在下一小姐陳述。

三、IOCP開發的幾個概念 

第一、伺服器的吞吐量問題。

      我們都知道,基于IOCP的開發是異步IO的,也正是這一技術的本質,決定了IOCP所實作的伺服器的高吞吐量。

       我們舉一個及其簡化的例子,來說明這一問題。在網絡伺服器的開發過程中,影響其性能吞吐量的,有很多因素,在這裡,我們隻是把關注點放在兩個方面,即:網 絡IO速度與Disk IO速度。我們假設:在一個千兆的網絡環境下,我們的網絡傳輸速度的極限是大概125M/s,而Disk IO的速度是10M/s。在這樣的前提下,慢速的Disk 裝置會成為我們整個應用的瓶頸。我們假設線程A負責從網絡上讀取資料,然後将這些資料寫入Disk。如果對Disk的寫入是同步的,那麼線程A在等待寫完 Disk的過程是不能再從網絡上接受資料的,在寫入Disk的時間内,我們可以認為這時候Server的吞吐量為0(沒有接受新的用戶端請求)。對于這樣 的同步讀寫Disk,一些的解決方案是通過增加線程數來增加伺服器處理的吞吐量,即:當線程A從網絡上接受資料後,驅動另外單獨的線程來完成讀寫Disk 任務。這樣的方案缺點是:需要線程間的合作,需要線程間的切換(這是另一個我們要讨論的問題)。而IOCP的異步IO本質,就是通過作業系統核心的支援, 允許線程A以非阻塞的方式向IO子系統投遞IO請求,而後馬上從網絡上讀取下一個用戶端請求。這樣,結果是:在不增加線程數的情況下,IOCP大大增加了 伺服器的吞吐量。說到這裡,聽起來感覺很像是DMA。的确,許多軟體的實作技術,在本質上,與硬體的實作技術是相通的。另外一個典型的例子是硬體的流水線 技術,同樣,在軟體領域,也有很著名的應用。好像話題扯遠了,呵呵:)

第二、線程間的切換問題。 

         伺服器的實作,通過引入IOCP,會大大減少Thread切換帶來的額外開銷。我們都知道,對于伺服器性能的一個重要的評估名額就 是:System/Context Switches,即機關時間内線程的切換次數。如果在每秒内,線程的切換次數在千的數量級上,這就意味着你的伺服器性能值得商榷。Context Switches/s應該越小越好。說到這裡,我們來重新審視一下IOCP。

     完成端口的線程并發量可以在建立該完成端口時指定(即NumberOfConcurrentThreads參數)。該并發量限制了與該完成端口相關聯的可 運作線程的數目(就是前面我在IOCP簡介中提到的執行者線程組的最大數目)。當與該完成端口相關聯的可運作線程的總數目達到了該并發量,系統就會阻塞任 何與該完成端口相關聯的後續線程的執行,直到與該完成端口相關聯的可運作線程數目下降到小于該并發量為止。最有效的假想是發生在有完成包在隊列中等待,而 沒有等待被滿足,因為此時完成端口達到了其并發量的極限。此時,一個正在運作中的線程調用GetQueuedCompletionStatus時,它就會 立刻從隊列中取走該完成包。這樣就不存在着環境的切換,因為該處于運作中的線程就會連續不斷地從隊列中取走完成包,而其他的線程就不能運作了。

     完成端口的線程并發量的建議值就是你系統CPU的數目。在這裡,要區厘清楚的是,完成端口的線程并發量與你為完成端口建立的工作者線程數是沒有任何關系 的,工作者線程數的數目,完全取決于你的整個應用的設計(當然這個不宜過大,否則失去了IOCP的本意:))。

第三、IOCP開發過程中的消息亂序問題。 

     使用IOCP開發的問題在于它的複雜。我們都知道,在使用TCP時,TCP協定本身保證了消息傳遞的次序性,這大大降低了上層應用的複雜性。但是當使用

IOCP

三個線程同時從IOCP中讀取Msg1, Msg2,與Msg3。由于TCP本身消息傳遞的有序性,是以,在IOCP隊列内,Msg1-Msg2-Msg3保證了有序性。三個線程分别從IOCP中 取出Msg1,Msg2與Msg3,然後三個線程都會将各自取到的消息投遞到邏輯層處理。在邏輯處理層的實作,我們不應該假定Msg1-Msg2- Msg3順序,原因其實很簡單,在Time 1~Time 2的時間段内,三個線程被作業系統排程的先後次序是不确定的,是以在到達邏輯處理層,

Msg1,Msg2與Msg3的次序也就是不确定的。是以,邏輯處理層的實作,必須考慮消息亂序的情況,必須考慮多線程環境下的程式實作。

        在這裡,我把消息亂序的問題單列了出來。其實在IOCP的開發過程中,相比于同步的方式,應該還有其它更多的難題需要解決,這也是與Select方式相比,IOCP的缺點,實作複雜度高。

結束語:

    ACE的Proactor Framework, 對windows平台的IOCP做了基于Proactor設計模式的,面向對象的封裝,這在一定程度上簡化了應用開發的難度,是一個很好的異步IO的開發架構,推薦學習使用。 

Reference:

    Microsoft Technet,Inside I/O Completion Ports