兩篇文章看懂EventLoopGroup,EventLoop的設計和運行機制(一)

前言

學習初衷

今天分析的都是netty的內容,但是我自己還沒有真正用netty實戰過,我主要在用vert.x,一直想把vert.x的架構設計和線程模型搞得明明白白的,之前也看過一些源碼,但我覺得沒有徹徹底底地搞清楚,這次花點心思搞明白。但線程模型裏面最重要的就是eventLoop了,所以最近一直在學習。
最核心的類就是NIOEventLoop了,也搜索過很多博客,裏面也有不少將這個類從頭到尾一行一行代碼分析地明明白白的。但是這樣又有什麼用呢?這樣就算是學會了EventLoop了嗎?遠遠不夠。

學習核心

學習EventLoop的話,核心有如下幾點:

  1. 它的設計需求是什麼。
  2. 它的類圖是如何設計的?每一個接口或者抽象類的職責是什麼,它們之間的區別和聯繫是啥?
  3. 它的內部運行機制(最好用一張圖能表達清楚)。
  4. 它是如何把JAVA的NIO結合在自己的框架裏面的。

第一點:很重要,任何一種框架都有基本的設計需求,瞭解需求才能更好的理解整個框架。但是我們怎麼可能知道它的需求呢?確實不容易,能查就查點,其它的就靠多學習多實踐去體會了。
第二點:因爲它的類圖比較複雜,想學好它,就要做到這點。
第三點:如果你經常用它,它的內部運行機制必須要清楚,當你提交了一個任務,腦海裏面立馬能出現它的執行畫面,做到心中有數,完全不慌。沒有什麼是一張圖表達不清楚的,如果不能,說明理解的不到位。
第四點:它基於Java原生的NIO,但是也做了很多封裝。那它到底做了什麼?爲什麼要做這些工作?

學習它的每一行代碼是爲了上面這幾點服務的,能夠讓自己的理解更加到位,說的就是更簡單一點就是總結;說實話,我雖然也看了很多遍源碼,當時也都看明白了,但是過幾天好多細節就忘光光了。對我而言主要是可能大腦覺得沒有什麼用,就自動遺忘了。事實也是如此,但是如果說自己做了總結,把非常關鍵的設計和機制瞭解清楚,然後以可視化的方式表達出來,這效果可能非常好。

目前自己可能只對二、三比較瞭解,這和自己的學習初衷有關係,我是來研究vert.x的,不是來學習netty的,搞清楚這幾點就夠了。不過第四點,我覺得後續還是有必要學習的。當了解後面幾點了,就可以去反推它的設計需求了,然後跟自己的理解不斷碰撞,反覆推敲,然後一瞬間恍然大悟~~~

類圖的學習

先放一張所有博客都會出現的圖吧:
在這裏插入圖片描述
確實很複雜,但是很遺憾,我幾乎沒有看見過對這個類圖進行詳細分析的博客,僅僅只是講NIOEventLoop這個類的核心代碼,包括比較火爆的netty書籍《Netty權威指南(第2版)》也沒有。但是你不覺得這很重要嗎?道行比較深的人,只要把類圖分析清楚了,它的設計和機制基本也瞭然於胸了。
當然我也沒有做到,剛開始我也打算這樣做,但是因爲類圖中的一個點,覺得不可能思議,根本不合理,就放棄了,也是先看了NIOEventLoop類的所有代碼以後,纔回頭去分析的。當然,最後也解答了內心的疑惑,後面會提到。
那麼如何來分析這個類圖呢?
我推薦有兩個步驟:刪減和增補。
刪減就是把最下面的實現類全部刪除掉,僅僅保留最上面的一些接口定義,比如:
在這裏插入圖片描述
就是EventExecutorGroup,這個接口是netty自定義的第一個接口,然後看了一下這個接口,我發現它本身就有自己的實現,並不是NIOEventLoopGroup,所以我又採取了增補的方式,把上面的類圖補充了一下:
在這裏插入圖片描述
(話說IDEA的類圖生成功能還是可以的)
從這個圖裏面就能夠清晰的看到有兩條線:

NIOEventLoopGroup,NIOEventLoop
以及
DefaultEventExecutorGroup,DefaultEventExecutor
對應的接口定義分別是:
EventLoopGroup,EventLoop
EventExecutorGroup,EventExecutor
並且從他們的層次關係上面可以看出來,前者都是繼承了後者的。
所以我的結論來了:先分析和學習EventExecutorGroup,EventExecutor,把這個學懂了,再學習EventLoop。一定會事半功倍。

當然接下來我先不接着分析類圖了,提上面的類圖只是想引出EventExecutor,然後再提出我學習這塊的總結,因爲裏面包含了它,直接提出來感覺有點突兀(捂臉表情)。
所以接下來先說結論,結論說完了,再說我的分析和學習過程。

先上結論:EventLoop的運行機制

這邊重點給大家分享一下它的運行機制,本篇幅不會分享太多細節的東西,因此結論裏面有些內容看不懂的話,可以先看後面的具體分析。

EventExecutorGroup和EventExecutor

簡單來說,EventExecutorGroup就是一個線程池,EventExecutor是它裏面執行任務的最小單元事件執行器。
它的運行機制圖如下:
在這裏插入圖片描述
這兩個接口的默認實現上面提到過,上面的機制肯定來自於具體的實現類,可以對照這具體實現類進行看。
解釋一下圖裏面的重點內容:

  1. EventExecutorGroup裏面包含了n個EventExecutor,n需要在初始化的時候就指定。
  2. 在EventExecutorGroup提交的任務,它都會選擇組內一個EventExecutor(順序輪詢選擇)去執行,所以運行機制的重點是EventExecutor。
  3. EventExecutor內部會和一個線程進行關聯,當提交第一個任務的時候,線程啓動並且不停運行,除非外界執行關閉操作。
  4. EventExecutor內部有兩個隊列:taskQueue和scheduleTaskQueue,前者是一個阻塞隊列,存儲提交的普通task(用execute或者submit方法提交的);後者是一個優先級隊列(線程不安全),存儲的是調度任務(用schedule方法提交的),它會把最早應該執行的任務放在隊首,保證peek出來的一定是隊列優先級最高的。
  5. 提交普通task的時候,直接加進隊列;提交定時任務的時候,當前線程是關聯線程的話,加到調度隊列中,否則提交一個普通task,task的內容就是:把調度任務加入調度隊列中。
  6. 關聯的線程一直在循環做一件事情:取任務,然後執行。
  7. 取任務的過程,圖中應該比較清晰,它的具體代碼實現是在類SingleThreadEventExecutor的takeTask()方法,可以結合在一起看。

關於組和成員的思考

EventExecutorGroup是個組,而EventExecutor是組裏面的成員,那麼它是如何來管理的呢?這也是我在學習過程中比較關心的點,也是我剛開始看類圖的時候比較疑惑的點。

特點描述

組和成員之間的關係有如下特點:

  • 組擁有成員的一部分功能
    • 其中一些功能的具體實現需要依靠成員去完成,比如提交任務
    • 其中一些功能與成員表示的含義層次不同,比如關閉等方法。
  • 成員擁有組的所有功能
  • 成員比組多一些額外的功能

基於如上特點,纔有了EventExecutorGroup與EventExecutor的繼承關係。
當組的功能大於成員,並且放在成員上特別不合適的時候(比如類似於next和iterator這些的,成員方法實現這些方法的時候,直接返回的就是本身),繼承關係是不是就應該反過來了,或許吧,還沒有見過。

最大疑惑

剛開始看類圖的時候,我最想不明白的地方在於它們的繼承關係,我的直覺上認爲正確的應該是反着的:EventExecutorGroup應該繼承EventExecutor。所以才導致分析類圖非常辛苦。

根源在於我平時經常使用的策略模式,策略模式它必須有一個上下文對象來維護對實際策略的一個引用。然後外界通過調用上下文的某個方法然後去真正執行實際的策略。
爲了更方便直觀的調用(暫且就這個理由吧),我通常喜歡上下文也實現一下策略接口,這樣的話,隱隱約約就會出現一種感覺:上下文在管理着策略;當策略更具體一些(具體的應用場景)的時候,這種感覺就會明顯。
這種感覺導致的是:管理者需要實現成員的接口。這也是我剛看這邊類圖的時候的最大的疑惑。
最後硬着頭皮學習完以後,我覺得我理解了。上面有關策略模式的感覺是錯誤的,它並沒有存在一種管理的含義,僅僅只是一種聚合而已,更像是一種代理。和這邊組以及成員的關係不一樣。

不過是不是也會有“當組的功能大於成員”的情況呢?繼承關係是會反過來的吧。

EventLoopGroup與EventLoop的關係也是如此。

ThreadPoolExecutor的運行機制

當然我意識到EventExecutorGroup是一個線程池的時候,腦子裏面立馬就想起來了ThreadPoolExecutor,這不就是我們經常用的線程池嗎?那他們有什麼區別呢。我就順便做了一下對比。
能用一張圖說明白的事情,絕不大篇幅描述;先上一個圖:
在這裏插入圖片描述
關於這個類的分析,包括運行機制和每一行的代碼,博客還是比較多的,大家說的都挺對的。我不做具體分析了,簡單強調幾個我認爲比較重要的點吧。

  • 線程分爲核心和非核心,但是它並沒有真正去賦予線程這樣一個屬性,而且當每一個線程執行任務結束以後通過當前運行的線程數與核心線程數的大小比較而得來的,運行的多了,那你這個線程就是非核心的,反之,你就是核心線程。
  • 針對不同線程類型,它們從隊列裏面取任務的策略也會有所不同,默認情況下,核心線程會使用take()方法無限阻塞從隊列取任務,取不出來我就不行,一直等着。非核心線程會使用pool(long,TimeUnit)方法進行超時獲取,超時時間是初始化時候配置的KeepAliveTime,如果超過這個點,還沒有取到任務,說明任務有點少,那麼當前線程就可以銷燬了。這也就是最大存活時間了。
  • 但是如果將屬性allowCoreThreadTimeOut置爲true的話,核心線程的特殊待遇也就沒有了,所有線程都一樣了,都會超時獲取,然後銷燬。但一般不會這樣配置的。

但是通過圖解和解釋,也不一定能夠真正用懂線程池,看一下Java推薦的用法吧:
在這裏插入圖片描述
如果我們自己去配置參數,其實還是不太容易的,但是有一些屬性,我們可以獲取到,比如當
corePooSize=x,maxPoolSize=y,queue.size=z
那麼你這個線程池正常情況下,會有x個線程去處理,也就是最多有x個任務正在執行,其它的都在等着。
任務比較多的時候,同時提交的任務超過了(x+z)個,那麼同時可能會有x<n<=y個線程正在執行,n個任務被執行。但是這種配置僅支持(y+z)個任務在同一時刻執行。
說的也挺抽象的,但是可能會有一些用,比如配置tomcat或者quartz的參數的時候,如果自己要配置的話,根據自己情況調整吧。
當然支持(y+z)個任務執行並不是每s可以支撐這麼多,它指的是同一時刻,那每秒有多少的話,就看任務的執行時間,比如每個任務只要10ms,那每秒就能處理100*(y+z)個任務。

EventExecutorGroup和ThreadPoolExecutor的比較

還是上圖,相同點的話,並不是很嚴格,不用太糾結,哈哈。
在這裏插入圖片描述
都在圖裏面了,就不解釋了。
既然有兩個線程池了,那麼應該用哪個呀?當然是ThreadPoolExecutor了,毫不猶豫。EventExecutorGroup畢竟是netty的實現,非netty的地方,應該用不到它,而且它在netty裏面的使用場景也不多,確實有,但是那塊還沒有仔細看過。從功能上來說,還是jdk提供的功能多一些。這個一點都不糾結。

EventLoopGroup和EventLoop

前面的學習完了,然後再來分析EventLoop,這也是我實際的學習過程。
EventLoop裏面會出現一個多路複用器(選擇器)Selector,這個是Java中Nio的內容,這邊學習的話,一定要至少把NIO的server端和client端的交互的demo跑一跑,再順便理解一下幾種常見的IO模型,再來學習會比較合適。這邊不解釋NIO的一些概念。
老樣子,先上個圖:
在這裏插入圖片描述
(圖中暫時忽略了調度任務,對於這個類而言重點是處理IO和非IO,儘管它對定時任務的處理方式和上面講的EventExecutor的方式不一樣)

這邊強調一下,雖然講的是EventLoop,但是它這個接口裏面並沒有定義任何有關多路複用器的相關方法,雖然有幾個註冊通道的方法,但和多路複用器並沒有直接的聯繫,這一點我還沒有去深究。我最起碼看到過,netty在使用的時候,有的接口定義的雖然是EventLoop,但是在使用的時候,會強轉成NIOEventLoop,比如AbstractNioChannel。
這邊強調一下圖中的關鍵內容:

  • EventLoop裏面關鍵屬性有兩個,多路複用器Selector和任務隊列。可以把通道(Channel)註冊在多路複用器上面,可以不斷輪詢其中的事件然後執行。任務隊列存儲提交的task。
  • EventLoop處理的事件(叫任務也行,事件更加貼切吧)整體上有兩種:
    • IO事件。當一個EventLoop所關聯的多路複用器上面註冊的通道發生“連接、接收(Acceptor)、讀、寫”事件的時候,就相當於觸發了IO事件。一般也就兩種場景:作爲server端的時候,監聽一個端口,別人來訪問你的端口,就會先觸發接收事件,然後讀取,寫入事件。作爲client端的時候,要和目標連接,連接成功以後就會觸發連接事件,然後寫入,讀取事件。(場景簡化了一下)
    • 非IO事件。這邊又分爲兩種:
      • 普通任務。使用execute提交的任務,直接執行的。
      • 調度任務。使用schedule提交的任務,一般需要延遲或者週期性執行的。
  • EventLoop在執行的時候,也是無線循環,循環體內主要有3件事:阻塞輪詢、執行IO事件和執行非IO事件。
    • 若當前沒有任務非IO事件(普通任務)需要執行,且在0.5s內沒有需要執行的調度任務的時候,先會進入一個無限循環,裏面會調用多路複用器的select(long)方法進行阻塞超時輪詢,阻塞超時默認是1s或者有定時任務的話,就取定時任務應該執行的時間與當前時間的間隔爲超時時間(意思就是,我超時結束的時候,最早的定時任務剛好可以執行了)。
    • 多路複用器的阻塞超時輪詢,並不會一直等到超時,有多種方式可以喚醒它:
      • 多路複用器已經準備好了至少一個事件;基本上就是有IO事件的話,就直接返回了,不會阻塞。
      • 使用wakeup方法。當其它線程調用的時候,會立刻喚醒正在阻塞輪詢多路複用器的線程。而EventLoop也是利用了這一點,當有新的任務提交進來,並且當前情況滿足4個條件的話,就會執行wakeUp。條件很好滿足。而且其中某些條件就是在判斷是不是在做阻塞輪詢,如果是的話,纔會去喚醒。
      • 當正在阻塞輪詢的時候,有新的非IO任務進來的話,就會立刻喚醒。和上一點是一回事,換了一種說法。
      • 這邊也有一個騷操作,它在執行一些中斷操作的時候,會提交一個空任務來喚醒。
      • 超時時間到。
      • 當前線程被中斷。後面這兩種沒有什麼可說的。
    • 它的這種喚醒機制,保證了不會影響到任何事件。但是仔細想想,這也是應該的,畢竟是它實在沒有事情做的時候,纔回去阻塞輪詢,因爲對於NIO而已,根本不需要進行阻塞,你去忙你的,忙完了回來叫我,我都給你準備好了,你忙你的,我做我的,相互不影響(你=eventLoop,我=多路複用器)。正因爲如此,它的代碼實現上面,對於跳出無限阻塞輪詢(阻塞輪詢外層有個無限循環)的條件也是非常開放(不知道怎麼描述了),很容易就跳出了,可以看看代碼。
    • 阻塞輪詢完了或者根本不需要阻塞輪詢的(有非IO事件),就要處理事件了。它這邊有個IO比例,默認是50,就是IO:非IO=50:50,比如處理IO的時間是100ms,那麼處理非IO的時間最大也得是100ms,但是它並沒有強行去限制,也確實不好做。它僅僅只是在每執行64個非IO事件以後去判斷一下這個時間,超了的話,就停下來。64,也不知道是怎麼定義的,說實話我覺得挺多的,太小的話,是不是就會影響到非IO任務的執行了呢?還有這個IO比例,當=100的時候,就完全忽略了時間比,每輪詢一次,就會把剩餘的所有非IO全部執行完。既然都是IO比例了,這種情況就不應該是隻執行IO嗎?只執行IO肯定不對,但是這個實現和對應的情況實在是不搭呀,理解不了。或許是因爲有些事情我還沒有理解透徹。
    • 執行IO的時候,就是把所有輪詢到的事件,挨個去執行。這塊就是我開篇提到的第四個核心,不過我還沒有細看(主要是挺複雜的,不花點事情是搞不明白的),就不說了。反正是一個一個執行IO事件,而且肯定是用當前線程去執行,但是肯定不會花太多時間去處理完的,到最後一定會交給另外一個EventLoopGroup,這也是標準的Reactor模型。netty服務端啓動的時候,需要提供兩個EventLoopGroup,也是這個作用吧,我猜的。
    • 執行非IO的時候,先把調度隊列中所有到期的取出來放進任務隊列中,然後挨個去執行。一個是全部執行完,一個有時間限制。執行完了以後,會執行tailTasks隊列裏面的任務,這個設計不知道用來幹嘛的,意思就是每一次輪詢結束,就去執行一下。感覺沒有什麼用呀。
    • 結束以後,下一波輪詢又開始了。
  • 它再內部阻塞輪詢多路複用器的時候,也修復了JDK的epoll bug。
    • bug描述:它會導致Selector空輪詢,IO線程CPU 100%,嚴重影響系統的安全性和可靠性。
    • 修復思路:
      • 根據該BUG的特徵,首先偵測該BUG是否發生:正常情況下,開始時間+阻塞輪詢時間<=當前時間;這個是正常的;但是如果反過來的話,就不正常了。實際上阻塞的時間比預期的時間會小,不符合javadoc的描述,就認爲做了一次空輪詢。當空輪詢次數超過默認值512次時,就去重新構建多路複用器。
      • 將問題Selector上註冊的Channel轉移到新建的Selector上
      • 老的問題Selector關閉,使用新建的Selector替換
  • 它內部還有一個比較重要的原子性的布爾值:wakeUp。它是用來確定是否需要喚醒正在使用阻塞輪詢多路複用器的線程(就是EventLoop的線程)。
    • true:代表應該被喚醒或者已經被喚醒了(它有的地方會判斷爲ture的時候,會立即喚醒,之後也不會修改它的狀態)
    • false:代表應該去阻塞輪詢了或者正在阻塞輪詢。
    • 修改的它的位置有3個:
      • 開始打算輪詢的時候,會置爲false(select(boolean)方法)。代表我馬上要阻塞輪詢了。
      • 在無限輪詢的循環體內,每次都會判斷:有新任務並且是false的時候,會置爲true,然後跳出。這個應該是來解決,當添加任務不滿足4個條件的時候,就不會觸發喚醒;這個是每次阻塞輪詢前判斷,也就是有的任務添加進來,雖然不會立即喚醒阻塞輪詢線程,但是當阻塞結束的時候,它一定就會跳出循環。結束,有新任務進來了。
      • 添加任務的時候,如果是false,會置爲true。wakeup(boolean inEventLoop)。添加任務需要觸發喚醒,需要滿足4個條件。

到這邊,EventLoop的運行機制應該是講清楚了吧,雖然說好多細節沒有體現,也沒有分析代碼。但是所有的機制,基本上我都是看了很多遍代碼才得來的結論。不過還是覺得最起碼有兩點可以繼續研究一下,比如NIO那邊的具體處理邏輯,以及整體的關閉策略,畢竟人家是優化了JDK的關閉接口的。

看下一篇吧

篇幅有點多了,本來還想繼續分享類圖的,也就是我實際的閱讀代碼的過程,再寫一篇分享吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章