這章包涵以下內容
- 線程模型概覽
- 事件循環(huán)概念和實現(xiàn)
- 任務調度
- 實現(xiàn)細節(jié)
簡單地說收恢,線程模型指定了OS帖族、編程語言武契、框架或應用程序的上下文中的線程管理的關鍵方面募判。線程創(chuàng)造的方式和時間明顯對于應用程序代碼的執(zhí)行有著重大的影響,所以開發(fā)人員有必要去理解與不同模型相關的權衡咒唆。無論他們自己選擇模型還是通過采用框架語言隱式獲取模型届垫,都是無可厚非的。
這一章我們將仔細研究Netty的線程模型全释。它是功能強大而易于使用的装处,正和Netty一樣,致力于簡化你的應用程序代碼并使其性能和可維護性盡可能最大化浸船。同時我們將探討引導我們選擇當前模型的經(jīng)歷妄迁。
如果你對于Java并發(fā)API(java.util.concurrent)有著融會貫通的理解寝蹈,你應該發(fā)現(xiàn)這一章的探討是簡單明了的。如果你對這些概念是陌生的或者你需要重新喚起你對它們的記憶登淘,Brian Goetz等人所著的Java Concurrency in Practice(Addison-Wesley Professional箫老,2006)是一個相當不錯的資源。
7.1 線程模型概覽
這一節(jié)我們將從整體上介紹線程模型形帮,然后探討Netty過去和現(xiàn)在的線程模型槽惫,回顧它們的優(yōu)點和缺陷。
正如我們在這章開頭提及的辩撑,線程模型指定代碼如何被執(zhí)行界斜。因為我們必須一直監(jiān)視并發(fā)執(zhí)行可能產生的副作用,理解所應用模型的含義是非常重要的(單線程模型也一樣)合冀。如果你忽略這些問題且僅僅希冀于最好的各薇,無異于賭博——毫無疑問對你不利。
因為多核或多個CPUs的電腦隨處可見君躺,大多數(shù)現(xiàn)代應用程序使用復雜的多線程技術來充分利用系統(tǒng)資源峭判。相比而言,我們在Java早期的多線程方案不外乎按需創(chuàng)建和開啟新的線程來執(zhí)行工作的并發(fā)單元棕叫,這是一個重負載下工作效率低下的原始方案林螃。Java 5隨之引入了Executor API,此API中的線程池通過Thread緩存和重用極大提高了性能俺泣。
基礎的線程池模式可以被描述為:
- 從線程池空閑列表選擇Thread然后把它分配去運行一個提交的任務(Runnable接口的實現(xiàn))疗认。
- 當任務完成,Thread返回線程池空閑列表且變成可重用的伏钠。
圖7.1闡明了這一模式横漏。
圖 7.1 Executor執(zhí)行邏輯
線程池和可重用線程相比于每個任務創(chuàng)建和銷毀線程是一個改進,但是它并沒有消除上下文切換的成本熟掂,當線程數(shù)量增加到處于極度重負載時缎浇,這種成本消耗愈加明顯。此外赴肚,在項目的生命周期過程中僅僅因為應用程序的整體復雜性或并發(fā)需求素跺,其他線程相關的問題可能出現(xiàn)。
簡而言之誉券, 多線程是復雜的亡笑。下一節(jié)我們將發(fā)現(xiàn)Netty如何幫助簡化多線程。
EventLoop 接口
運行任務來處理在連接的生命周期過程中發(fā)生的事件横朋,這是任何網(wǎng)絡框架的基本方法。相應的編碼結構通常被稱為事件循環(huán)百拓,Netty從接口io.netty.channel.EventLoop采用的術語琴锭。
以下代碼清單闡明了事件循環(huán)基本的想法晰甚,每一個任務是一個Runnable實例(如圖7.1所示)。
碼單 7.1 以事件循環(huán)方式執(zhí)行任務
while (!terminated) {
//blocks until there are events that are ready to run
List<Runnable> readyEvents = blockUntilEventsReady();
for (Runnable ev: readyEvents) {
ev.run();
}
}
Netty的EventLoop是協(xié)作設計的一部分决帖,此設計采用兩個基礎的APIs:并發(fā)和網(wǎng)絡厕九。首先,包io.netty.util.concurrent建立在JDK包java.util.concurrent上提供線程執(zhí)行器地回。其次扁远,為了連接Channel事件,包io.netty.channel中的類繼承這些執(zhí)行器刻像。最終的類層次結構如圖7.2所示畅买。
在這個模型中,一個EventLoop恰好被一個Thread持有细睡,并且不會改變谷羞,任務(Runnable或Callable)可以直接提交到EventLoop實現(xiàn)以立即執(zhí)行或調度執(zhí)行。根據(jù)配置和可用內核溜徙,為了使資源利用最大化湃缎,多個EventLoops可能被創(chuàng)建,且單個EventLoop可能被分配去服務多個Channels蠢壹。
注意Netty的這個EventLoop嗓违,當它繼承ScheduledExecutorService,僅僅定義一個方法parent()图贸。這個顯示在以下代碼塊中的方法蹂季,目的是返回當前EventLoop實現(xiàn)實例所屬的EventLoopGroup的引用。
public interface EventLoop extends EventExecutor, EventLoopGroup {
@Override
EventLoopGroup parent();
}
圖 7.2 EventLoop類層次結構
事件/任務執(zhí)行順序事件和任務以FIFO(先進先出)順序執(zhí)行求妹。通過保證字節(jié)內容以正確順序處理消除數(shù)據(jù)損壞的可能性乏盐。
7.2.1 Netty 4中I/O和事件處理
正如我們在第六章詳細描述的,I/O操作流通已經(jīng)建立一個或多個ChannelHandlers的ChannelPipeline會觸發(fā)事件制恍。傳播這些事件的方法調用可以被ChannelHandlers攔截并按需處理事件父能。
事件的性質通常決定它被處理的方式;它有可能從網(wǎng)絡堆棧傳送數(shù)據(jù)到你的應用程序净神,相反何吝,也可能做一些完全不同的事。但是事件處理的邏輯必須是通用的且足夠復雜以處理所有可能的用例鹃唯。因此爱榕,在Netty 4所有I/o操作和事件被已經(jīng)分配給EventLoop的線程處理。
這與Netty 3中使用的模型不同坡慌。下一節(jié)我們將討論更早的模型和它為什么被替換黔酥。
7.2.2 Netty 3中的I/O操作
在先前版本使用的線程模型僅僅保證入站(先前叫做上游)事件在所謂的I/O線程(與Netty 4的EventLoop對應)中會被執(zhí)行。所有出站(下游)事件被調用的線程處理,此線程可能是I/O線程或者任何其他線程跪者。起初這看起來是一個好主意棵帽,但由于ChannelHandlers中出站事件的仔細同步的需要而被發(fā)現(xiàn)是有問題的。簡而言之渣玲,不可能保證多線程不會在同一時間嘗試去訪問入站事件逗概。這是可能發(fā)生的,舉個例子忘衍,如果你通過在不同線程中調用Channel.write()逾苫,觸發(fā)同一個Channel的同時發(fā)生的下游事件。
當入站事件被觸發(fā)導致出站事件時枚钓,另外一個消極放方面的影響出現(xiàn)铅搓。當Channel.write()導致異常,你有必要去生成并觸發(fā)exceptionCaught事件秘噪。但在Netty 3模型中狸吞,因為這是入站事件,你最終在調用線程中執(zhí)行代碼指煎,然后通過執(zhí)行I/O線程處理事件蹋偏,隨之而來的是多余的上下文切換。
Netty 4中采用的線程模型至壤,通過處理發(fā)生在同一線程所給定的EventLoop的每件事威始,解決了這些問題。此模型提供了一個更簡便的執(zhí)行架構且消除了ChannelHandlers中的同步需要(除了多個Channels之間可能共享的部分)像街。
既然你理解了EventLoop的角色黎棠,讓我們看看任務是如何調度執(zhí)行的。
7.3 任務調度
有時候你有必要去調度一個任務更遲的(延期的)或定期的執(zhí)行镰绎。舉個例子脓斩,你可能想去注冊一個任務,并在客戶端已經(jīng)連接5分鐘后觸發(fā)它畴栖。一個普遍的用例是發(fā)送心態(tài)信息給遠端随静,以此檢查連接是否仍然活躍。如果沒有回應吗讶,你就知道你可以關閉通道了燎猛。
在下一節(jié)中,我們將向你展示如何用核心的Java API和Netty的EventLoop調度任務照皆。然后重绷,我們將檢查Netty的內部實現(xiàn)并討論它的優(yōu)缺點。
7.3.1 JDK調度API
在Java 5之前膜毁,任務調度以java.util.Timer為基礎昭卓,它使用一個后臺線程愤钾,且有和標準的線程相同的限制。隨后候醒,JDK提供了包java.util.concurrent绰垂,它定義了接口ScheduledExecutorService。表7.1展示了java.util.concurrent.Executors的相關工廠方法火焰。
表 7.1 java.util.concurrent.Executors工廠方法
方法 | 描述 |
---|---|
newScheduledThreadPool(int corePoolSize) / newScheduledThreadPool(int corePoolSize,ThreadFactorythreadFactory) | 創(chuàng)建一個可以調度命令在延遲后運行或定期執(zhí)行的ScheduledThreadExecutorService。它使用參數(shù)corePoolSize來計算線程數(shù)量胧沫。 |
newSingleThreadScheduledExecutor() / newSingleThreadScheduledExecutor(ThreadFactorythreadFactory) | 創(chuàng)建一個可以調度命令在延遲后運行或定期執(zhí)行的ScheduledThreadExecutorService昌简。它使用一個線程執(zhí)行調度的任務。 |
雖然選擇不是很多绒怨,但對于大多數(shù)用例來說這些提供的選擇已經(jīng)足夠了纯赎。以下代碼清單展示了如何使用ScheduledExecutorService在60s延遲后運行任務。
碼單 7.2 用ScheduledExecutorService調度任務
ScheduledExecutorService executor =
Executors.newScheduledThreadPool(10);
ScheduledFuture<?> future = executor.schedule(
new Runnable() {
@Override
public void run() {
System.out.println("60 seconds later");
}
}, 60, TimeUnit.SECONDS);
...
executor.shutdown();
雖然ScheduledExecutorService API簡單明了南蹂,但是重負載下它能帶來性能成本犬金。下一節(jié)我們將看到Netty如何用更高的效率提供相同的功能。
7.3.2 使用EventLoop調度任務
ScheduledExecutorService實現(xiàn)有不足之處六剥,比如額外的線程被創(chuàng)建為線程池管理的一部分晚顷。如果任務沒有被積極地調用,這可能成為一個性能瓶頸疗疟。Netty通過使用通道的EventLoop實現(xiàn)調度來解決這一問題该默,正如以下代碼清單所示。
碼單 7.3 用EventLoop調度任務
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().schedule(
new Runnable() {
@Override
public void run() {
System.out.println("60 seconds later");
}
}, 60, TimeUnit.SECONDS);
60s過后策彤,Runnable實例將被分配給通道的EventLoop執(zhí)行栓袖。如果要調度一個任務使其每間隔60s執(zhí)行,使用scheduleAtFixedRate()店诗,如下所示裹刮。
碼單 7.4 用EventLoop調度循環(huán)任務
Channel ch = ...
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(
new Runnable() {
@Override
public void run() {
System.out.println("Run every 60 seconds");
}
}, 60, 60, TimeUnit.Seconds);
正如我們之前提到的,Netty的EventLoop繼承ScheduledExecutorService(看圖7.2)庞瘸,所以它提供了JDK實現(xiàn)的所有可用方法捧弃,包括上述例子中用到的schedule()和scheduleAtFixedRate()。所有操作的完整清單可以在關于ScheduledExecutorService的Javadocs中找到恕洲。
使用每一個異步操作返回的ScheduledFuture來取消或者檢查執(zhí)行狀態(tài)塔橡。以下代碼清單占了一個簡單的取消操作。
碼單 7.5 使用ScheduledFuture取消任務
ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(...);
// Some other code that runs...
boolean mayInterruptIfRunning = false;
//cancels the task, which prevents it from running again
future.cancel(mayInterruptIfRunning);
這些例子闡明性能可以通過充分利用Netty的調度能力進行獲取霜第。反過來葛家,這些依賴于底層線程模型,接下來我們將討論它泌类。
7.4 實現(xiàn)細節(jié)
這一節(jié)進一步詳細討論Netty線程模型和調度實現(xiàn)的主要因素癞谒。我們還會提到需要注意的缺陷底燎,以及持續(xù)發(fā)展的領域。
7.4.1 線程管理
Netty線程模型的優(yōu)越性能取決于確定當前正在執(zhí)行的Thread的標識弹砚;也就是說双仍,不論它被分配給當前Channel和它的EventLoop。(回想一下桌吃,EventLoop有責任為Channel處理其生命周期期間所有的事件朱沃。)
如果調用的線程是EventLoop的,有問題的代碼被執(zhí)行茅诱。否則逗物,EventLoop調度任務來延遲執(zhí)行并把它放到一個內部的隊列中。當EventLoop接著執(zhí)行它的事件瑟俭,它將執(zhí)行在隊列中的這些事件翎卓。這闡釋了在ChannelHandlers沒有取得同步的情況下,Thread如何直接與Channel相互作用的摆寄。
注意每一個EventLoop有它自己的任務隊列失暴,并不依賴于任何其他EventLoop。圖7.3展示了EventLoop用來調度任務的執(zhí)行邏輯微饥。這是Netty線程模型的一個關鍵的組件逗扒。
我們之前講了不要阻塞當前I/O線程的重要性。我們用另一種方式再陳述一遍:“千萬不要將長期運行的線程放到執(zhí)行隊列中,因為它會阻塞任何其他的任務在同一線程執(zhí)行◎保”如果你必須阻塞調用或者執(zhí)行長期運行的任務,我們建議使用專用的EventExecutor蛮拔。(查看側邊欄章節(jié)6.2.1中的“ChannelHandler執(zhí)行和阻塞”)
圖 7.3 EventLoop執(zhí)行邏輯
暫且不說這種極限情況,線程模型在使用中可以強烈影響排隊任務對整體系統(tǒng)性能的影響痹升。正如所使用的傳輸事件處理實現(xiàn)建炫。(和我們在第四章看到的一樣,Netty在不借助于修改你的代碼庫的情況下疼蛾,可以方便地切換傳輸肛跌。)
7.4.2 EventLoop/線程配置
服務Channels的I/O和事件的EventLoops被包含在一個EventLoopGroup中。EventLoops被創(chuàng)建和分配的方式根據(jù)傳輸實現(xiàn)變化察郁。
異步傳輸
異步實現(xiàn)僅僅使用很少的EventLoops(和它們相關的線程)衍慎,并在當前模型中這些可以在Channels之間共享。這允許很多Channels由盡可能少的Threads服務皮钠,而不是每一個Channel分配一個Thread稳捆。
圖7.4顯示了一個EventLoopGroup,其大小固定為三個EventLoopsge(每個由一個線程驅動)麦轰。當EventLoopGroup被創(chuàng)建時乔夯,EventLoops(和它們的線程)直接被分配砖织,以此確保它們在被需要的時候是可用的。
EventLoopGroup負責分配EventLoop給每一個新創(chuàng)建的Channel末荐。在目前的實現(xiàn)中侧纯,使用循環(huán)方法實現(xiàn)均衡分布,且同樣的EventLoop會分配到多個Channels甲脏。(這一點在未來的版本中可能會改善眶熬。)
圖7.4 非阻塞傳輸?shù)腅ventLoop分配(比如NIO和AIO)
一旦一個Channel已經(jīng)被分配給一個EventLoop,它將使用這個EventLoop(和相關的線程)貫穿其生命周期块请。記住這一點聋涨,因為它使你不用擔心在你的ChannelHandler實現(xiàn)中的線程安全和同步。
同樣负乡,請注意EventLoop分配對ThreadLocal使用的影響。因為一個EventLoop通常驅動不止一個Channel,ThreadLocal對于所有相關的Channels都是一樣的脊凰。這使得實現(xiàn)一個方法抖棘,比如狀態(tài)跟蹤,不是一個很好的選擇狸涌。然而切省,在一個無狀態(tài)的上下文中,對于共享它仍然可用于Channels之間共享繁重或昂貴的對象帕胆,甚至事件朝捆。
阻塞傳輸
其他傳輸?shù)脑O計比如IOI(古老的阻塞I/O)是有一點不同的,如圖7.5所闡明的懒豹。
圖7.5 阻塞傳輸?shù)腅ventLoop 分配(比如OIO)
這里一個EventLoop(和它的線程)分配給一個Channel芙盘。如果你已經(jīng)使用java.io包中的阻塞I/O來開發(fā)應用程序,你可能已經(jīng)遇到這種模式脸秽。
但使用這種模式之前儒老,你必須保證每個Channel的I/O事件僅僅被一個驅動此Channel的EventLoop的線程處理。這是Netty的一致性設計的另一例子记餐,并且它對Netty的可靠性和易用性有很大貢獻驮樊。
7.5 總結
在這章中你學習了通常的線程模型和特別的Netty線程模型,我們詳細討論了后者的性能和一致性優(yōu)點片酝。
你明白了如何用EventLoop(I/O Thread)去執(zhí)行你自己的任務囚衔,正如框架自身所做的。你學會了如何去調度延遲執(zhí)行的任務雕沿,并且我們研究了重負載下的可擴展性問題练湿。你也知道如何去驗證一個任何是否已經(jīng)執(zhí)行和如何去取消它。
這些由我們對框架實現(xiàn)細節(jié)的研究拓展的信息晦炊,將幫助你使得你的應用程序性能最大化同時簡化它的代碼庫鞠鲜。更多關于線程池和并發(fā)編程的信息宁脊,我們推薦Brian Goetz所著的Java Concurrency in Practice(java并發(fā)實戰(zhàn))。這本書將給你對最復雜多線程用例的深度理解贤姆。
我們已經(jīng)到了一個激動人心的時刻——下一章我們將討論Bootstrapping榆苞,一個為你的應用程序帶來生命的配置和連接所有Netty組件的處理過程。