本章介紹java
線程模型(thread-model)編程
事件循環(EventLoop)安全
併發(Concurrency)網絡
任務執行(task execution)數據結構
任務調度(task scheduling)多線程
線程模型定義了應用程序或框架如何執行你的代碼,選擇應用程序/框架的正確的線程模型是很重要的。Netty提供了一個簡單強大的線程模型來幫助咱們簡化代碼,Netty對全部的核心代碼都進行了同步。全部ChannelHandler,包括業務邏輯,都保證由一個線程同時執行特定的通道。這並不意味着Netty不能使用多線程,只是Netty限制每一個鏈接都由一個線程處理,這種設計適用於非阻塞程序。咱們沒有必要去考慮多線程中的任何問題,也不用擔憂會拋ConcurrentModificationException或其餘一些問題,如數據冗餘、加鎖等,這些問題在使用其餘框架進行開發時是常常會發生的。併發
讀完本章就會深入理解Netty的線程模型以及Netty團隊爲何會選擇這樣的線程模型,這些信息可讓咱們在使用Netty時讓程序由最好的性能。此外,Netty提供的線程模型還可讓咱們編寫整潔簡單的代碼,以保持代碼的整潔性;咱們還會學習Netty團隊的經驗,過去使用其餘的線程模型,如今咱們將使用Netty提供的更容易更強大的線程模型來開發。框架
儘管本章講述的是Netty的線程模型,可是咱們仍然可使用其餘的線程模型;至於如何選擇一個完美的線程模型應該根據應用程序的實際需求來判斷。異步
本章假設以下:socket
你明白線程是什麼以及如何使用,並有使用線程的工做經驗;若不是這樣,就請花些時間來了解清楚這些知識。推薦一本書:Java併發編程實戰。
你瞭解多線程應用程序及其設計,也包括如何保證線程安全和獲取最佳性能。
你瞭解java.util.concurrent以及ExecutorService和ScheduledExecutorService。
15.1 線程模型概述
本節將簡單介紹通常的線程模型,Netty中如何使用指定的線程模型,以及Netty不一樣的版本中使用的線程模型。你會更好的理解不一樣的線程模型的全部利弊。
若是思考一下,在咱們的生活中會發現不少狀況都會使用線程模型。例如,你有一個餐廳,向你的客戶提供食品,食物須要在廚房煮熟後才能給客戶;某個客戶下了訂單後,你須要將煮熟事物這個任務發送到廚房,而廚房能夠以不一樣的方式來處理,這就像一個線程模型,定義瞭如何執行任務。
只有一個廚師:
這種方法是單線程的,一次只執行一個任務,完成當前訂單後再處理下一個。
你有多個廚師,每一個廚師均可以作,空閒的廚師準備着接單作飯:
這種方式是多線程的,任務由多個線程(廚師)執行,能夠並行同時執行。
你有多個廚師並分紅組,一組作晚餐,一個作其餘:
這種狀況也是多線程,可是帶有額外的限制;同時執行多個任務是由實際執行的任務類型(晚餐或其餘)決定。
從上面的例子看出,平常活動適合在一個線程模型。可是Netty在這裏適用嗎?
不幸的是,它沒有那麼簡單,Netty的核心是多線程,但隱藏了來自用戶的大部分。
Netty使用多個線程來完成全部的工做,只有一個線程模型線型暴露給用戶。大多數現代應用程序使用多個線程調度工做,讓應用程序充分使用系統的資源來有效工做。在早期的Java中,這樣作是經過按需建立新線程並行工做。但很快發現者不是完美的方案,由於建立和回收線程須要較大的開銷。
在Java5中加入了線程池,建立線程和重用線程交給一個任務執行,這樣使建立和回收線程的開銷降到最低。
下圖顯示使用一個線程池執行一個任務,提交一個任務後會使用線程池中空閒的線程來執行,完成任務後釋放線程並將線程從新放回線程池.
上圖每一個任務線程的建立和回收不須要新線程去建立和銷燬,但這只是一半的問題,咱們稍後學習。你可能會問爲何不使用多線程,使用一個ExecutorService能夠有助於防止線程建立和回收的成本?
使用多線程會有太多的上下文切換,提升了資源和管理成本,這種反作用會隨着運行線程的數量和執行的任務數量的增長而越發明顯。使用多線程在剛開始可能沒有什麼問題,但隨着系統的負載增長,可能在某個點就會讓系統崩潰。
除了這些技術上的限制和問題,在項目生命週期內維護應用程序/框架可能還會發生其餘問題。它有效的說明了增長應用程序的複雜性取決於它是平行的,簡單的陳述:編寫多線程應用程序時一個辛苦的工做!咱們怎麼來解決這個問題呢?在實際的場景中須要多個線程模型。讓咱們來看看Netty是如何解決這個問題的。
15.2 事件循環
事件循環所作的正如它的名字,它運行的事件在一個循環中,直到循環終止。這很是適合網絡框架的設計,由於它們須要爲一個特定的鏈接運行一個事件循環。這不是Netty的新發明,其餘的框架和實現已經很早就這樣作了。
在Netty中使用EventLoop接口表明事件循環,EventLoop是從EventExecutor和ScheduledExecutorService擴展而來,因此能夠講任務直接交給EventLoop執行。類關係圖以下:
15.2.1 使用事件循環
下面代碼顯示如何訪問已分配給通道的EventLoop並在EventLoop中執行任務:
Channel ch = ...; ch.eventLoop().execute(new Runnable() { @Override public void run() { System.out.println("run in the eventloop"); } });
使用事件循環的好處是不須要擔憂同步問題,在同一線程中執行全部其餘關聯通道的其餘事件。這徹底符合Netty的線程模型。檢查任務是否已執行,使用返回的Future,使用Future能夠訪問不少不一樣的操做。下面的代碼是檢查任務是否執行:
Channel ch = ...; Future<?> future = ch.eventLoop().submit(new Runnable() { @Override public void run() { } }); if(future.isDone()){ System.out.println("task complete"); }else { System.out.println("task not complete"); }
檢查執行任務是否在事件循環中:
Channel ch = ...; if(ch.eventLoop().inEventLoop()){ System.out.println("in the EventLoop"); }else { System.out.println("outside the EventLoop"); }
只有確認沒有其餘EventLoop使用線程池了才能關閉線程池,不然可能會產生未定義的反作用。
15.2.2 Netty4中的I/O操做
這個實現很強大,甚至Netty使用它來處理底層I/O事件,在socket上觸發讀和寫操做。這些讀和寫操做是網絡API的一部分,經過java和底層操做系統提供。下圖顯示在EventLoop上下文中執行入站和出站操做,若是執行線程綁定到EventLoop,操做會直接執行;若是不是,該線程將排隊執行:
須要一次處理一個事件取決於事件的性質,一般從網絡堆棧讀取或傳輸數據到你的應用程序,有時在另外的方向作一樣的事情,例如從你的應用程序傳輸數據到網絡堆棧再發送到遠程對等通道,但不限於這種類型的事物;更重要的是使用的邏輯是通用的,靈活處理各類各樣的案例。
應該指出的是,線程模型(事件循環的頂部)描述並不老是由Netty使用。咱們在瞭解Netty3後會更容易理解爲何新的線程模型是可取的。
15.2.3 Netty3中的I/O操做
在之前的版本有點不一樣,Netty保證在I/O線程中只有入站事件才被執行,全部的出站時間被調用線程處理。這看起來是個好方案,但很容易出錯。它還將負責同步ChannelHandler來處理這些事件,由於它不保證只有一個線程同時操做;這可能發生在你去掉通道下游事件的同時,例如,在不一樣的線程調用Channel.write(...)。下圖顯示Netty3的執行流程:
除了須要負擔同步ChannelHandler,這個線程模型的另外一個問題是你可能須要去掉一個入站事件做爲一個出站事件的結果,例如Channel.write(...)操做致使異常。在這種狀況下,捕獲的異常必須生成並拋出去。乍看之下這不像是一個問題,但咱們知道,捕獲異常由入站事件涉及,會讓你知道問題出在哪裏。問題是,事實上,你如今的狀況是在調用線程上執行,但捕獲到異常事件必須交給工做線程來執行。這是可行的,但若是你忘了傳遞過去,它會致使線程模型失效;假設入站事件只有一個線程不是真,這可能會給你各類各樣的競爭條件。
之前的實現有一個惟一的積極影響,在某些狀況下它能夠提供更好的延遲;成本是值得的,由於它消除了複雜性。實際上,在大多數應用程序中,你不會遵照任何差別延遲,還取決於其餘因數,如:
字節寫入到遠程對等通道有多快
I/O線程是否繁忙
上下文切換
鎖定
你能夠看到不少細節影響總體延遲。
15.2.4 Netty線程模型內部
Netty的內部實現使其線程模型表現優異,它會檢查正在執行的線程是不是已分配給實際通道(和EventLoop),在Channel的生命週期內,EventLoop負責處理全部的事件。若是線程是相同的EventLoop中的一個,討論的代碼塊被執行;若是線程不一樣,它安排一個任務並在一個內部隊列後執行。一般是經過EventLoop的Channel只執行一次下一個事件,這容許直接從任何線程與通道交互,同時還確保全部的ChannelHandler是線程安全,不須要擔憂併發訪問問題。
下圖顯示在EventLoop中調度任務執行邏輯,這適合Netty的線程模型:
設計是很是重要的,以確保不要把任何長時間運行的任務放在執行隊列中,由於長時間運行的任務會阻止其餘在相同線程上執行的任務。這多少會影響整個系統依賴於EventLoop實現用於特殊傳輸的實現。傳輸之間的切換在你的代碼庫中可能沒有任何改變,重要的是:切勿阻塞I/O線程。若是你必須作阻塞調用(或執行須要長時間才能完成的任務),使用EventExecutor。
下一節將講解一個在應用程序中常用的功能,就是調度執行任務(按期執行)。Java對這個需求提供瞭解決方案,但Netty提供了幾個更好的方案。
15.3 調度任務執行
每隔一段時間須要調度任務執行,也許你想註冊一個任務在客戶端完成鏈接5分鐘後執行,一個常見的用例是發送一個消息「你還活着?」到遠程對等通道,若是遠程對等通道沒有反應,則能夠關閉通道(鏈接)和釋放資源。就像你和朋友打電話,沉默了一段時間後,你會說「你還在嗎?」,若是朋友沒有回覆,就多是斷線或朋友睡着了;不論是什麼問題,你均可以掛斷電話,沒有什麼可等待的;你掛了電話後,收起電話能夠作其餘的事。
本節介紹使用強大的EventLoop實現任務調度,還會簡單介紹Java API的任務調度,以方便和Netty比較加深理解。
15.3.1 使用普通的Java API調度任務
在Java中使用JDK提供的ScheduledExecutorService實現任務調度。使用Executors提供的靜態方法建立ScheduledExecutorService,有以下方法:
newScheduledThreadPool(int)
newScheduledThreadPool(int, ThreadFactory)
newSingleThreadScheduledExecutor()
newSingleThreadScheduledExecutor(ThreadFactory)
看下面代碼:
ScheduledExecutorService executor = Executors.newScheduledThreadPool(10); ScheduledFuture<?> future = executor.schedule(new Runnable() { @Override public void run() { System.out.println("now it is 60 seconds later"); } }, 60, TimeUnit.SECONDS); if(future.isDone()){ System.out.println("scheduled completed"); } //..... executor.shutdown();
15.3.2 使用EventLoop調度任務
使用ScheduledExecutorService工做的很好,可是有侷限性,好比在一個額外的線程中執行任務。若是須要執行不少任務,資源使用就會很嚴重;對於像Netty這樣的高性能的網絡框架來講,嚴重的資源使用是不能接受的。Netty對這個問題提供了很好的方法。
Netty容許使用EventLoop調度任務分配到通道,以下面代碼:
Channel ch = ...; ch.eventLoop().schedule(new Runnable() { @Override public void run() { System.out.println("now it is 60 seconds later"); } }, 60, TimeUnit.SECONDS);
若是想任務每隔多少秒執行一次,看下面代碼:
Channel ch = ...; ScheduledFuture<?> future = ch.eventLoop().scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("after run 60 seconds,and run every 60 seconds"); } }, 60, 60, TimeUnit.SECONDS); // cancel the task future.cancel(false);
15.3.3 調度的內部實現
Netty內部實現實際上是基於George Varghese提出的「Hashed and hierarchical timing wheels: Data structures to efficiently implement timer facility(散列和分層定時輪:數據結構有效實現定時器)」。這種實現只保證一個近似執行,也就是說任務的執行可能不是100%準確;在實踐中,這已經被證實是一個可容忍的限制,不影響多數應用程序。因此,定時執行任務不可能100%準確的按時執行。
爲了更好的理解它是如何工做,咱們能夠這樣認爲:
在指定的延遲時間後調度任務;
任務被插入到EventLoop的Schedule-Task-Queue(調度任務隊列);
若是任務須要立刻執行,EventLoop檢查每一個運行;
若是有一個任務要執行,EventLoop將馬上執行它,並從隊列中刪除;
EventLoop等待下一次運行,從第4步開始一遍又一遍的重複。
由於這樣的實現計劃執行不可能100%正確,對於多數用例不可能100%準備的執行計劃任務;在Netty中,這樣的工做幾乎沒有資源開銷。可是若是須要更準確的執行呢?很容易,你須要使用ScheduledExecutorService的另外一個實現,這不是Netty的內容。記住,若是不遵循Netty的線程模型協議,你將須要本身同步併發訪問。
15.4 I/O線程分配細節
Netty使用線程池來爲Channel的I/O和事件服務,不一樣的傳輸實現使用不一樣的線程分配方式;異步實現是隻有幾個線程給通道之間共享,這樣可使用最小的線程數爲不少的平道服務,不須要爲每一個通道都分配一個專門的線程。
下圖顯示如何分配線程池:
如上圖所示,使用一個固定大小的線程池管理三個線程,建立線程池後就把線程分配給線程池,確保在須要的時候,線程池中有可用的線程。這三個線程會分配給每一個新建立的已鏈接通道,這是經過EventLoopGroup實現的,使用線程池來管理資源;實際會平均分配通道到全部的線程上,這種分佈以循環的方式完成,所以它可能不會100%準確,但大部分時間是準確的。
一個通道分配到一個線程後,在這個通道的生命週期內都會一直使用這個線程。這一點在之後的版本中可能會被改變,因此咱們不該該依賴這種方式;不會被改變的是一個線程在同一時間只會處理一個通道的I/O操做,咱們能夠依賴這種方式,由於這種方式能夠確保不須要擔憂同步。
下圖顯示OIO(Old Blocking I/O)傳輸:
從上圖能夠看出,每一個通道都有一個單獨的線程。咱們可使用java.io.*包裏的類來開發基於阻塞I/O的應用程序,即便語義改變了,但有一件事仍然保持不變,每一個通道的I/O在同時只能被一個線程處理;這個線程是由Channel的EventLoop提供,咱們能夠依靠這個硬性的規則,這也是Netty框架比其餘網絡框架更容易編寫的緣由。
15.5 Summary
本章主要講解Netty的線程模型,其核心接口是EventLoop;並和OIO中的線程模型作了比較,以突顯Netty的優異性。