首頁  >  文章  >  Java  >  Java中關於IO復用的圖文詳解

Java中關於IO復用的圖文詳解

黄舟
黄舟原創
2017-05-28 09:23:001427瀏覽

這篇文章主要介紹了Java IO復用的相關知識,非常不錯,具有參考借鑒價值,需要的的朋友參考下吧

對於伺服器的並發處理能力,我們需要的是:每一毫秒伺服器都能及時處理這一毫秒內收到的數百個不同TCP連接上的報文,與此同時,可能伺服器上還有數以十萬計的最近幾秒沒有收發任何報文的相對不活躍連線。同時處理多個並行發生事件的連接,簡稱為並發;同時處理萬計、十萬計的連接,則是高並發。伺服器的並發程式所追求的就是處理的並發連線數目無限大,同時維持著高效率使用CPU等資源,直到實體資源先耗盡。

並發程式設計有很多種實作模型,最簡單的就是與「執行緒」捆綁,1個執行緒處理1個連線的全部生命週期。優點:這個模型夠簡單,它可以實現複雜的業務場景,同時,執行緒個數是可以遠大於CPU個數的。然而,線程個數又不是可以無限增大的,為什麼呢?因為線程什麼時候執行是由操作系統內核調度演算法決定的,調度演算法並不會考慮某個線程可能只是為了一個連接服務的,它會做大一統的玩法:時間片到了就執行一下,哪怕這個線程一執行就會不得不繼續睡眠。這樣來回的喚醒、睡眠線程在次數不多的情況下,是廉價的,但如果操作系統的線程總數很多時,它就是昂貴的(被放大了),因為這種技術性的調度損耗會影響到線程上執行的業務代碼的時間。舉個例子,這時大部分擁有不活躍連線的執行緒就像我們的國企,它們執行效率太低了,它總是喚醒就睡眠在做無用功,而它喚醒爭到CPU資源的同時,就意味著處理活躍連線的民企執行緒減少獲得了CPU的機會,CPU是核心競爭力,它的無效率進而影響了GDP總吞吐量。我們所追求的是並發處理數十萬連接,當幾千個線程出現時,系統的執行效率就已經無法滿足高並發了。

對高並發編程,目前只有一種模型,也是本質上唯一有效的玩法。連線上的消息處理,可分為兩個階段:等待訊息準備好、訊息處理。當使用預設的阻塞套接字時(例如上面提到的1個執行緒捆綁處理1個連線),往往是把這兩個階段合而為一,這樣操作套接字的程式碼所在的執行緒就得睡眠來等待訊息準備好,這導致了高並發下執行緒會頻繁的睡眠、喚醒,從而影響了CPU的使用效率。

高並發程式設計方法當然就是把兩個階段分開處理。即,等待訊息準備好的程式碼段,與處理訊息的程式碼段是分離的。當然,這也要求套接字必須是非阻塞的,否則,處理訊息的程式碼片段很容易導致條件不滿足時,所在執行緒又進入了睡眠等待階段。那麼問題來了,等待訊息準備好這個階段怎麼實現?它畢竟還是等待,這意味著線程還是要睡眠的!解決辦法就是,主動查詢,或讓1個執行緒為所有連線而等待!這就是IO多路復用了。多路復用就是處理等待訊息準備好這件事的,但它可以同時處理多個連線!它也可以“等待”,所以它也可能導致線程睡眠,然而這不要緊,因為它一對多、它可以監控所有連接。這樣,當我們的執行緒被喚醒執行時,就一定是有一些連線準備好被我們的程式碼執行了,這是有效率的!沒有那麼多線程都在爭搶處理「等待訊息準備好」階段,整個世界終於清淨了!
多重化有很多種實現,在linux上,2.4核心前主要是select和poll,現在主流是epoll,它們的使用方法似乎很不同,但本質是一樣的。

效率卻也不同,這也是epoll完全取代了select的原因。

簡單的談下epoll為何會取代select。

前面提到過,高並發的核心解決方案是1個線程處理所有連接的“等待訊息準備好”,這一點上epoll和select是無爭議的。但select預估錯誤了一件事,就像我們開篇所說,當數十萬並發連接存在時,可能每一毫秒只有數百個活躍的連接,同時其餘數十萬連接在這一毫秒是非活躍的。 select的使用方法是這樣的:
傳回的活躍連線 ==select(全部待監控的連線)

什麼時候會呼叫select方法呢?在你認為需要找出有報文到達的活躍連結時,就應該呼叫。所以,呼叫select在高並發時是會被頻繁呼叫的。這樣,這個頻繁調用的方法就很有必要看看它是否有效率,因為,它的輕微效率損失都會被「頻繁」二字所放大。它有效率損失嗎?顯而易見,全部待監控連接是數以十萬計的,返回的只是數百個活躍連接,這本身就是無效率的表現。被放大後就會發現,處理並發上萬個連結時,select就完全力不從心了。
看幾個圖。當並發連結為一千以下,select的執行次數不算頻繁,與epoll似乎並無多少差距: 

然而,並發數一旦上去, select的缺點被「執行頻繁」無限放大了,且並發數越多越明顯:

再來說說epoll是如何解決的。它很聰明的用了3個方法來實現select方法要做的事:

新建的epoll描述符==epoll_create()

epoll_ctrl(epoll描述符,添加或刪除所有待監控的連接)

返回的活躍連接==epoll_wait(epoll描述子)

這麼做的好處主要是:分清了頻繁調用和不頻繁呼叫的操作。例如,epoll_ctrl是不太頻繁呼叫的,而epoll_wait是非常頻繁地呼叫的。這時,epoll_wait卻幾乎沒有入參,這比select的效率高出一大截,而且,它也不會隨著並發連接的增加使得入參越發多起來,導致內核執行效率下降。

epoll是怎麼實現的呢?其實很簡單,從這3個方法就可以看出,它比select聰明的避免了每次頻繁調用「哪些連接已經處在訊息準備好階段」的epoll_wait時,是不需要把所有待監控連接傳入的。這意味著,它在內核態維護了一個資料結構保存所有待監控的連線。這個資料結構就是一棵紅黑樹,它的結點的增加、減少是透過epoll_ctrl來完成的。它是非常簡單的: 

圖中左下方的紅黑樹由所有待監控的連結所構成。左上方的鍊錶,同是目前所有活躍的連結。於是,epoll_wait執行時只是檢查左上方的鍊錶,並返回左上方鍊錶中的連接給用戶。這樣,epoll_wait的執行效率能不高嗎?

最後,再看看epoll提供的2種玩法ET和LT,也就是翻譯過來的邊緣觸發和水平觸發。其實這兩個中文名字倒也有些貼切。這2種使用方式針對的仍然是效率問題,只不過變成了epoll_wait返回的連接如何能夠更準確些。

例如,我們需要監控一個連線的寫緩衝區是否空閒,滿足「可寫」時我們就可以從使用者狀態將回應呼叫write傳送給客戶端 。但是,或連線可寫時,我們的「回應」內容還在磁碟上呢,此時若是磁碟讀取還沒完成呢?肯定不能使線程阻塞的,那麼就不發送回應了。但是,下次epoll_wait時可能又把這個連線回傳給你了,你還得檢查下是否要處理。可能,我們的程式有另一個模組專門處理磁碟IO,它會在磁碟IO完成時再發送回應。那麼,每次epoll_wait都回傳這個「可寫」的、卻無法立刻處理的連接,是否符合使用者預期呢?

於是,ET和LT模式就應運而生了。 LT是每次滿足期待狀態的連接,都得在epoll_wait中返回,所以它一視同仁,都在一條水平線上。 ET則不然,它傾向於更精確的返回連接。在上面的例子中,連接第一次變成可寫後,若是程式未寫入連接上寫入任何數據,那麼下一次epoll_wait是不會回傳這個連接的。 ET叫做 邊緣觸發,就是指,只有連線從一個狀態轉到另一個狀態時,才會觸發epoll_wait回傳它。可見,ET的編程要複雜不少,至少應用程式要小心的防止epoll_wait的返回的連接出現:可寫時未寫數據後卻期待下一次“可寫”、可讀時未讀盡數據卻期待下一次「可讀」。

當然,從一般應用場景上它們性能是不會有什麼大的差距的,ET可能的優點是,epoll_wait的調用次數會減少一些,某些場景下連接在不必要喚醒時不會被喚醒(此喚醒指epoll_wait返回)。但如果像我上面舉例所說的,有時它不單純是網路問題,跟應用場景相關。當然,大部分開源框架都是基於ET寫的,框架嘛,它追求的是純技術問題,當然力求盡善盡美

#

以上是Java中關於IO復用的圖文詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn