簡單地說,線程模型指定了操做系統、編程語言、框架或者應用程序的上下文中的線程管理的關鍵方面。Netty的線程模型強大但又易用,而且和Netty的一向宗旨同樣,旨在簡化你的應用程序代碼,同時最大限度地提升性能和可維護性。java
一、線程模型概述編程
線程模型肯定了代碼的執行方式,因爲咱們老是必須規避併發執行可能會帶來的反作用,因此理解所採用的併發模型(也有單線程的線程模型)的影響很重要。緩存
由於具備多核心或多個CPU的計算機如今已經司空見慣,大多數的現代應用程序都利用了複雜的多線程處理技術以有效地利用系統資源。相比之下,在早期的Java語言中,咱們使用多線程處理的主要方式無非是按需建立和啓動新的Thread來執行併發的任務單元——一種在高負載下工做得不好的原始方式。Java 5隨後引入了Executor API,其線程池經過緩存和重用Thread極大地提升了性能。安全
基本的線程池化模式能夠描述爲:網絡
——從池的空閒線程列表中選擇一個Thread,而且指派它去運行一個已提交的任務(一個Runnable的說笑呢)多線程
——當任務完成時,將該Thread返回給該列表,使其可被重用。架構
下圖說明了這個模型 併發
雖然池化和重用線程相對簡單地爲每一個任務都建立和銷燬線程是一種進步,可是它並不能消除由上下文切換所帶來的開銷,其將隨着線程數量的增長很快變得明顯,而且在高負載下愈演愈烈。此外,僅僅因爲應用程序的總體複雜性或者併發需求,在項目的聲明週期內也可能會出現其餘的線程相關的問題。框架
二、EventLoop接口異步
運行任務來處理在鏈接的生命週期內發生的事件是任何網絡框架的基本功能。與之相應的編程上的構造一般被稱爲事件循環——一個Netty使用了Interface io.netty.channel.EventLoop來適配的術語。
如下代碼說明了事件循環的基本思想,其中的每一個任務都是一個Runnable的實例。
while(!terminated){ //阻塞,直到有事件已經就緒可被運行 List<Runnable> readyEvents = blockUntilEventReady(); for (Runnable ev:readyEvents){ //循環遍歷,並處理全部的事件 ev.run(); } }
Netty的EventLoop是協同設計的一部分,它採用了兩個基本的API:併發和網絡編程。首先,io.netty.util.concurrent包構建在JDK的java.util.concurrent包上,用來提供線程執行器。其次,io.netty.channel包中的類,爲了與Channel的事件進行交互,擴展了這些接口/類。
下圖展現了生成的類層次結構
在這個模型中,一個EventLoop將由一個永遠都不會改變的Thread驅動,同時任務(Runnable或者Callable)能夠直接提交給EventLoop實現,以當即執行或者調度執行。根據配置和可用核心的不一樣,可能會建立多個EventLoop實例用以優化資源的使用,而且單個EventLoop可能會被指派用於服務多個Channel。
須要注意的是,Netty的EventLoop在繼承了ScheduledExecutorService的同時,只定義了一個方法,parent(),用於返回到當前EventLoop實現的實例所屬的EventLoopGroup的引用。
事件/任務的執行順序:事件和任務是以先進先出(FIFO)的順序執行的,這樣能夠經過保證字節內容老是按正確的順序被處理,消除潛在的數據損壞的可能性。
三、Netty4中的I/O和事件處理
由I/O操做觸發的事件將流經安裝了一個或者多個ChannelHandler的ChannelPipeline。傳播這些事件的方法調用能夠隨後被ChannelHandler所攔截,而且能夠按需地處理事件。
事件的性質一般決定了它將被如何處理,他可能將數據從網絡棧中傳遞到你的應用程度中,或者進行逆向操做,或者執行一些大相徑庭的操做。可是事件的處理邏輯必須足夠的通用和靈活,以處理全部可能的用例。所以,在Netty4中,全部I/O操做和事件都由已經被分配給了EventLoop的那個Thread來處理。
四、Netty3中的I/O操做
在之前的版本中所使用的線程模型只保證了入站(以前稱爲上游)事件會在所謂的I/O線程(對應於Netty4中的EventLoop)中執行。全部的出站(下游)事件都由調用線程處理,其多是I/O線程也多是別的線程。開始看起來彷佛是好主意,可是已經被發現是有問題的,由於須要在ChannelHandler中對出站事件進行仔細的同步。簡而言之,不可能保證多個線程不會再同一時刻嘗試訪問出站事件。例如,若是你經過在不一樣的線程中調用Channel.write()方法,針對同一個Channel同時觸發出站的事件,就會發生這種狀況。
當出站事件觸發了入站事件時,將會致使另外一個負面影響。當Channel.write()方法致使異常時,須要生成並觸發一個exceptionCaught事件。可是在Netty3的模型中,因爲這是一個入站事件,須要在調用線程中執行代碼,而後將事件移交給I/O線程去執行,然而這將帶來額外的上下文切換。
Netty4中所採用的線程模型,經過在同一個線程中處理某個給定的EventLoop中所產生的全部事件,解決了這個問題。這提供了一個更加簡單的執行體系架構,而且消除了在多個ChannelHandler中進行同步的須要。
五、JDK的任務調度
你須要調度一個任務以便稍後(延遲)執行或者週期性地執行。例如,你可能想要註冊一個在客戶端已經鏈接了5分鐘以後觸發的任務。一個常見的用例是,發送心跳信息到遠程節點,以檢查鏈接是否仍然還活着。若是沒有響應,你便知道能夠關閉該Channel了。
在Java5以前,任務調度時創建在java.util.Timer類之上,其使用了一個後臺Thread,而且具備與標準線程相同的限制。隨後,JDK提供了java.util.concurrent包,它定義了interface ScheduledExecutorService。
雖然選擇很少,可是這些預置的實現已經足夠應對大多數的用例。
如下代碼展現瞭如何使用ScheduledExecutorService來在60秒的延遲以後執行一個任務。
//建立一個其線程池具備10個線程的ScheduledExecutorService ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10); //建立一個Runnable,以供調度稍後執行 ScheduledFuture<?> future = executorService.schedule(new Runnable() { @Override public void run() { //該任務要打印的消息 System.out.println("60 seconds later"); } //調度任務在從如今開始的60秒以後執行 },60, TimeUnit.SECONDS); ... //一旦調度任務執行完成,就關閉ScheduledExecutorService以釋放資源 executorService.shutdown();
雖然ScheduledExecutorService API是直接了當的,當時在高負載下它將帶來性能上的負擔。
六、使用EventLoop調度任務
ScheduledExecutorService的實現具備侷限性,例如,事實上做爲線程池管理的一部分,將會有額外的線程建立。若是有大量任務被緊湊地調度,那麼這將成爲一個瓶頸。Netty經過Channel的EventLoop實現任務調度解決了這一問題。
通過60秒以後,Runnable實例將由分配給Channel的EventLoop執行。若是要調度任務以每隔60秒執行一次,請使用sheduleAtFixedRate()方法,如如下代碼。
Channel ch = ...; ScheduledFuture<?> future = ch.eventLoop().schedule( //建立一個Runnable以供調度稍後執行 new Runnable() { @Override public void run() { //要執行的代碼 System.out.println("60 seconds later"); } //調度任務在從如今開始的60秒以後執行 },60,TimeUnit.SECONDS );
如前面提到的,Netty的EventLoop擴展了ScheduledExecutorService,全部它提供了JDK實現可用的全部方法,包括前面的schedule()和scheduleAtFixedRate()方法。全部操做的完整列表能夠在ScheduledExecutorService的Javadoc中找到。
要取消或者檢查(被調度任務)執行狀態,可使用每一個異步操做所返回的ScheduledFuture。
如下代碼展現了一個簡單的取消操做。
Channel ch = ...; //調度任務,並得到所返回的ScheduledFuture ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(...); //some other code that runs... boolean mayInterruptIfRunning = false; //取消該任務,防止它再次運行 future.cancel(mayInterruptIfRunning);
七、線程管理
Netty線程模型的卓越性能取決於對於當前執行的Thread的身份的肯定,也就是說,肯定它是否分配給當前Channel以及它的EventLoop的那一線程。
若是調用線程正是支撐EventLoop的線程,那麼所提交的代碼塊將會被執行。不然,EventLoop將調度該任務以便稍後執行,並將它放入到內部隊列中。當EventLoop下次處理它的事件時,它會執行隊列中的那些任務/事件。這也是任何的Thread是如何與Channel直接交互而無需在ChannelHandler中進行額外同步的。
注意,每一個EventLoop都有它本身的任務隊列,獨立於任何其餘的EventLoop。下圖展現了EventLoop用於調度任務的執行邏輯。這也是Netty線程模型的關鍵組成部分。
以前已經闡明瞭不要阻塞當前I/O線程的重要性。咱們再以另外一種方式重申一次:「永遠不要將一個長時間運行的任務放入到執行隊列中,由於它將阻塞須要在同一線程上執行的任何其餘任務。」若是必需要進行阻塞調用或者執行長時間運行的任務,咱們建議使用一個專門的EventExecutor。
除了這種受限的場景,如同傳輸所採用的不一樣的事件處理實現同樣,所使用的線程模型也能夠強烈地影響到排隊的任務對總體系統性能的影響。
八、EventLoop線程的分配——異步傳輸
異步傳輸實現只使用了少許的EventLoop,並且在當前的線程模型中,它們可能會被多個Channel所共享,這使得能夠經過儘量少許的Thread來支撐大量的Channel,而不是每一個Channel分配一個Thread。
下圖顯示了一個EventLoopGroup,它具備3個固定大小的EventLoop(每一個EventLoop都由一個Thread支撐)。在建立EventLoopGroup時就直接分配了EventLoop(以及支撐它們的Thread),以確保在須要時它們是可用的。
EventLoopGroup負責爲每一個新建立的Channel分配一個EventLoop。在當前實現中,使用順序循環(round-robin)的方式進行分配以獲取一個均衡的分佈,而且相同的EventLoop可能會被分配給多個Channel。
一旦一個Channel被分配給一個EventLoop,它將在它的整個生命週期中都是用這個EventLoop,請注意,由於它可使你從擔心你的ChannelHandler實現中的線程安全和同步問題中解脫出來。
另外,須要注意的是,EventLoop的分配方式對ThreadLocal的使用的影響。由於一個EventLoop一般會被用於支撐多個Channel,因此對於全部相關聯的Channel來講,ThreadLocal都將是同樣的。這使得它對於實現狀態追蹤等功能來講是個糟糕的選擇。然而,在一些無狀態的上下文中,它仍然能夠被用於在多個Channel之間共享一些重度的或者代價昂貴的對象,甚至是事件。
九、阻塞傳輸
用於像OIO(舊的阻塞I/O)這樣的其它傳輸的設計略有不一樣,以下圖所示。
這裏每個Channel都將被分配給一個EventLoop(以及他的Thread)。若是你開發的應用程序使用過java.io包中的阻塞I/O實現,你可能就遇到過這種模型。
可是,正如同以前同樣,獲得的保證是每一個Channel的I/O事件都只會被一個Thread(用於支撐該Channel的EventLoop的那個Thread)處理。這也是另外一個Netty設計一致性的例子。