兗州建設(shè)公司網(wǎng)站免費推廣網(wǎng)站大全
前言
簡單地說,線程模型指定了操作系統(tǒng)、編程語言、框架或者應(yīng)用程序的上下文中的線程管理的關(guān)鍵方面。顯而易見地,如何以及何時創(chuàng)建線程將對應(yīng)用程序代碼的執(zhí)行產(chǎn)生顯著的影響,因此開發(fā)人員需要理解與不同模型相關(guān)的權(quán)衡。無論是他們自己選擇模型,還是通過采用某種編程語言或者框架隱式地獲得它,這都是真實的。在這次,我們將會學(xué)習(xí)線程模型的概念。它強大但又易用,并且和Netty 的一貫宗旨一樣,旨在簡化你的應(yīng)用程序代碼,同時最大限度地提高性能和可維護(hù)性。我們還將討論致使選擇當(dāng)前線程模型的經(jīng)驗。
幸運的是,我之前讀過JAVA并發(fā)編程藝術(shù)這本書,所以我認(rèn)為這一章對我的提升應(yīng)該比較大
PS(個人理解,僅供參考,如有不對,權(quán)當(dāng)笑話):談到并發(fā)編程,很多時候大家都會自然想到高并發(fā),但是對我而言,所謂并發(fā)編程,其實就是將線程合理利用,我每天上班都會經(jīng)過很長的高速公路,其中有一段路特別的擁堵,但是他卻有四條車道,其他暢通的道路,可能只有兩條車道,有一天,我追根溯源,發(fā)現(xiàn)堵車的源頭是因為四條車道有一條在施工,我很好奇,四條路一條在施工,應(yīng)該有三條,那為什么兩條的反而不堵呢,我進(jìn)行了統(tǒng)計和特意的觀察,我發(fā)現(xiàn),四條車道的車流量極大,這就是所謂的并發(fā)量很大,4條線程在工作,有一條堵了,資源利用低,造成了卡頓。但是兩條路車流相對較好,也有好的設(shè)計,所以都能利用。這就是高并發(fā)的情況下,最大效率的利用資源會使得我們的程序表現(xiàn)良好,如何利用資源更好,顯然在很大的并發(fā)量的時候,有時候單線程不能很好的處理,所以就有了并發(fā)編程的概念,當(dāng)然并發(fā)編程并不僅僅運用于高并發(fā)場景,它可以在一些工具層有很好的表現(xiàn),例如讀取百萬數(shù)據(jù)并同步等場景。所以說高并發(fā)指的是系統(tǒng)并發(fā)量大,我們?nèi)绾稳コ休d這些并發(fā),最大限度的去將單機的吞吐量達(dá)到最大,而并發(fā)編程是如何提高程序效率。對我而言,基本概念是不同的。(可能隨著閱歷的增長,覺得這個觀點可笑,留作日后批評)。
線程模型概述
我們將介紹常見的線程模型,隨后將繼續(xù)討論Netty 過去以及當(dāng)前的線程模型,并評審它們各自的優(yōu)點以及局限性。,線程模型確定了代碼的執(zhí)行方式。由于我們總是必須規(guī)避并發(fā)執(zhí)行可能會帶來的副作用,所以理解所采用的并發(fā)模型(也有單線程的線程模型)的影響很重要。忽略這些問題,僅寄希望于最好的情況(不會引發(fā)并發(fā)問題)無疑是賭博——賠率必然會擊敗你。
因為具有多核心或多個CPU 的計算機現(xiàn)在已經(jīng)司空見慣,大多數(shù)的現(xiàn)代應(yīng)用程序都利用了復(fù)雜的多線程處理技術(shù)以有效地利用系統(tǒng)資源。相比之下,在早期的Java 語言中,我們使用多線程處理的主要方式無非是按需創(chuàng)建和啟動新的Thread 來執(zhí)行并發(fā)的任務(wù)單元——一種在高負(fù)載下工作得很差的原始方式。Java 5 隨后引入了Executor API,其線程池通過緩存和重用Thread 極大地提高了性能。
基本的線程池化模式可以描述為:
從池的空閑線程列表中選擇一個Thread,并且指派它去運行一個已提交的任務(wù)(一個Runnable 的實現(xiàn))
當(dāng)任務(wù)完成時,將該Thread 返回給該列表,使其可被重用。
雖然池化和重用線程相對于簡單地為每個任務(wù)都創(chuàng)建和銷毀線程是一種進(jìn)步,但是它并不能消除由上下文切換所帶來的開銷,其將隨著線程數(shù)量的增加很快變得明顯,并且在高負(fù)載下愈演愈烈。此外,僅僅由于應(yīng)用程序的整體復(fù)雜性或者并發(fā)需求,在項目的生命周期內(nèi)也可能會出現(xiàn)其他和線程相關(guān)的問題。
EventLoop 接口
運行任務(wù)來處理在連接的生命周期內(nèi)發(fā)生的事件是任何網(wǎng)絡(luò)框架的基本功能。與之相應(yīng)的編程上的構(gòu)造通常被稱為事件循環(huán)—一個Netty 使用了interface io.netty.channel.EventLoop 來適配的術(shù)語。
看一下這個接口吧:
public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {@OverrideEventLoopGroup parent();
}
繼承了兩個接口,里面就一個parent方法
private final EventExecutorGroup parent;private final Collection<EventExecutor> selfCollection = Collections.<EventExecutor>singleton(this);protected AbstractEventExecutor() {this(null);}protected AbstractEventExecutor(EventExecutorGroup parent) {this.parent = parent;}@Overridepublic EventExecutorGroup parent() {return parent;}
看樣子是返回了一個EventExecutorGroup。這個里面具體怎么工作的,這個EventLoop又是什么?
@Overrideprotected void run() {for (;;) {Runnable task = takeTask();if (task != null) {task.run();updateLastExecutionTime();}if (confirmShutdown()) {break;}}}
通過這個我看到了,其實就是一個阻塞隊列,然后自旋操作,什么時候有東西push進(jìn)去了,取出來,運行這個任務(wù),修改時間。很簡單呢的小玩意兒。
Netty 的EventLoop 是協(xié)同設(shè)計的一部分,它采用了兩個基本的API:并發(fā)和網(wǎng)絡(luò)編程。io.netty.util.concurrent是不是很像juc。這個包里面提供了很多執(zhí)行器的接口和類,其次io.netty.channe為了l與Channel進(jìn)行交互,擴展了這些類。一個EventLoop 將由一個永遠(yuǎn)都不會改變的Thread 驅(qū)動,同時任務(wù)(Runnable 或者Callable)可以直接提交給EventLoop 實現(xiàn),以立即執(zhí)行或者調(diào)度執(zhí)行。根據(jù)配置和可用核心的不同,可能會創(chuàng)建多個EventLoop 實例用以優(yōu)化資源的使用,并且單個EventLoop 可能會被指派用于服務(wù)多個Channel。
Netty 4 中的I/O 和事件處理
由I/O 操作觸發(fā)的事件將流經(jīng)安裝了一個或者多個ChannelHandler 的ChannelPipeline。傳播這些事件的方法調(diào)用可以隨后被ChannelHandler 所攔截,并且可以按需地處理事件。
事件的性質(zhì)通常決定了它將被如何處理;它可能將數(shù)據(jù)從網(wǎng)絡(luò)棧中傳遞到你的應(yīng)用程序中,或者進(jìn)行逆向操作,或者執(zhí)行一些截然不同的操作。但是事件的處理邏輯必須足夠的通用和靈活,以處理所有可能的用例。因此,在Netty 4 中,所有的I/O操作和事件都由已經(jīng)被分配給了EventLoop的那個Thread來處理。
Netty 3 中的I/O 操作
在以前的版本中所使用的線程模型只保證了入站(之前稱為上游)事件會在所謂的I/O 線程(對應(yīng)于Netty 4 中的EventLoop)中執(zhí)行。所有的出站(下游)事件都由調(diào)用線程處理,其可能是I/O 線程也可能是別的線程。開始看起來這似乎是個好主意,但是已經(jīng)被發(fā)現(xiàn)是有問題的,因為需要在ChannelHandler 中對出站事件進(jìn)行仔細(xì)的同步。簡而言之,不可能保證多個線程不會在同一時刻嘗試訪問出站事件。例如,如果你通過在不同的線程中調(diào)用Channel.write()方法,針對同一個Channel 同時觸發(fā)出站的事件,就會發(fā)生這種情況。當(dāng)出站事件觸發(fā)了入站事件時,將會導(dǎo)致另一個負(fù)面影響。當(dāng)Channel.write()方法導(dǎo)致異常時,需要生成并觸發(fā)一個exceptionCaught 事件。但是在Netty 3 的模型中,由于這是一個入站事件,需要在調(diào)用線程中執(zhí)行代碼,然后將事件移交給I/O 線程去執(zhí)行,然而這將帶來額外的上下文切換。Netty 4 中所采用的線程模型,通過在同一個線程中處理某個給定的EventLoop 中所產(chǎn)生的所有事件,解決了這個問題。這提供了一個更加簡單的執(zhí)行體系架構(gòu),并且消除了在多個ChannelHandler 中進(jìn)行同步的需要(除了任何可能需要在多個Channel中共享的)。
任務(wù)調(diào)度
偶爾,你將需要調(diào)度一個任務(wù)以便稍后(延遲)執(zhí)行或者周期性地執(zhí)行。例如,你可能想要注冊一個在客戶端已經(jīng)連接了5 分鐘之后觸發(fā)的任務(wù)。一個常見的用例是,發(fā)送心跳消息到遠(yuǎn)程節(jié)點,以檢查連接是否仍然還活著。如果沒有響應(yīng),你便知道可以關(guān)閉該Channel 了。
JDK 的任務(wù)調(diào)度API
在Java 5 之前,任務(wù)調(diào)度是建立在java.util.Timer 類之上的,其使用了一個后臺Thread,并且具有與標(biāo)準(zhǔn)線程相同的限制。隨后,JDK 提供了java.util.concurrent 包,它定義了interface ScheduledExecutorService。
使用EventLoop 調(diào)度任務(wù)
ScheduledExecutorService 的實現(xiàn)具有局限性,例如,事實上作為線程池管理的一部分,將會有額外的線程創(chuàng)建。如果有大量任務(wù)被緊湊地調(diào)度,那么這將成為一個瓶頸。Netty 通過Channel 的EventLoop 實現(xiàn)任務(wù)調(diào)度解決了這一問題。
public static void scheduleViaEventLoop() {Channel ch = new NioSocketChannel(); ScheduledFuture<?> future = ch.eventLoop().schedule(new Runnable() {@Overridepublic void run() {System.out.println("60 seconds later");}}, 60, TimeUnit.SECONDS);}
經(jīng)過60 秒之后,Runnable 實例將由分配給Channel 的EventLoop 執(zhí)行。如果要調(diào)度任務(wù)以每隔60 秒執(zhí)行一次,請使用scheduleAtFixedRate()方法。
淺看一下源碼。覺得自己又行了!
private <V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {if (inEventLoop()) {scheduleFromEventLoop(task);} else {final long deadlineNanos = task.deadlineNanos();// task will add itself to scheduled task queue when run if not expiredif (beforeScheduledTaskSubmitted(deadlineNanos)) {execute(task);} else {lazyExecute(task);// Second hook after scheduling to facilitate race-avoidanceif (afterScheduledTaskSubmitted(deadlineNanos)) {execute(WAKEUP_TASK);}}}return task;}
不解釋了 比較簡單。但是有必要學(xué)習(xí)一下執(zhí)行細(xì)節(jié)。
執(zhí)行細(xì)節(jié)
線程管理
Netty線程卓越的能力來自于堆當(dāng)前線程身份的判斷,也就是說,確定是否是分配給當(dāng)前Channel以及它的EventLoop的那一個線程。因為EventLoop將處理一個Channle所有的生命周期的事件。
如果(當(dāng)前)調(diào)用線程正是支撐EventLoop 的線程,那么所提交的代碼塊將會被(直接)執(zhí)行。否則,EventLoop 將調(diào)度該任務(wù)以便稍后執(zhí)行,并將它放入到內(nèi)部隊列中。當(dāng)EventLoop下次處理它的事件時,它會執(zhí)行隊列中的那些任務(wù)/事件。這也就解釋了任何的Thread 是如何與Channel 直接交互而無需在ChannelHandler 中進(jìn)行額外同步的。注意,每個EventLoop 都有它自已的任務(wù)隊列,獨立于任何其他的EventLoop。
··
注意注意:“永遠(yuǎn)不要將一個長時間運行的任務(wù)放入到執(zhí)行隊列中,因為它將阻塞需要在同一線程上執(zhí)行的任何其他任務(wù),”如果必須要進(jìn)行阻塞調(diào)用或者執(zhí)行長時間運行的任務(wù),我們建議使用一個專門的EventExecutor。
除了這種受限的場景,如同傳輸所采用的不同的事件處理實現(xiàn)一樣,所使用的線程模型也可以強烈地影響到排隊的任務(wù)對整體系統(tǒng)性能的影響
EventLoop/線程的分配
服務(wù)于Channel 的I/O 和事件的EventLoop 包含在EventLoopGroup 中。根據(jù)不同的傳輸實現(xiàn),EventLoop 的創(chuàng)建和分配方式也不同。
異步傳輸
異步傳輸實現(xiàn)只使用了少量的EventLoop(以及和它們相關(guān)聯(lián)的Thread),而且在當(dāng)前的線程模型中,它們可能會被多個Channel 所共享。這使得可以通過盡可能少量的Thread 來支撐大量的Channel,而不是每個Channel 分配一個Thread。
例如:有三個EventLoopGroup,每一個組中都會有幾個EventLoop,然后有新的Channel進(jìn)來,就給他分配一個EventLoop。每一個EventLoop都有一個線程關(guān)聯(lián),在當(dāng)前實現(xiàn)中,使用順序循環(huán)(round-robin)的方式進(jìn)行分配以獲取一個均衡的分布,并且相同的EventLoop可能會被分配給多個Channel。一旦一個Channel 被分配給一個EventLoop,它將在它的整個生命周期中都使用這個EventLoop(以及相關(guān)聯(lián)的Thread)。請牢記這一點,因為它可以使你從擔(dān)憂你的ChannelHandler 實現(xiàn)中的線程安全和同步問題中解脫出來。
另外,需要注意的是,EventLoop 的分配方式對ThreadLocal 的使用的影響。因為一個EventLoop 通常會被用于支撐多個Channel,所以對于所有相關(guān)聯(lián)的Channel 來說,ThreadLocal 都將是一樣的。這使得它對于實現(xiàn)狀態(tài)追蹤等功能來說是個糟糕的選擇。然而,在一些無狀態(tài)的上下文中,它仍然可以被用于在多個Channel 之間共享一些重度的或者代價昂貴的對象,甚至是事件。
阻塞傳輸
得到的保證是每個Channel 的I/O 事件都將只會被一個Thread(用于支撐該Channel 的EventLoop 的那個Thread)處理。
結(jié)束語
學(xué)習(xí)了Netty的線程模型和知道了EventLoop的原理,后面會專門在并發(fā)編程專欄中重點涉及一下線程模型,和工作中的應(yīng)用。