Netty專欄 (五)——— EventLoop和執行緒模型
Netty-In-Action
@author 魯偉林
記錄《Netty 實戰》中各章節學習過程,寫下一些自己的思考和總結,幫助使用Netty框架的開發技術人員們,能夠有所得,避免踩坑。
本部落格目錄結構將嚴格按照書本《Netty 實戰》,省略與Netty無關的內容,可能出現跳小章節。
本部落格中涉及的完整程式碼:
GitHub地址: https://github.com/thinkingfioa/netty-learning/tree/master/netty-in-action。
本人部落格地址: https://blog.csdn.net/thinking_fioa
第7章 EventLoop和執行緒模型
在本章開始前,推薦一本書籍《Java併發程式設計實戰》,此書應該是所有Java開發人員必讀書籍。
7.1 執行緒模型概述
基本的執行緒池化模式:
- 從執行緒池中的空閒執行緒列表中選擇一個Thread,指派去執行一個已提交的任務
- 任務完成後,該Thread重新回到執行緒池,使其可被重用
執行緒池通過快取和重用Thread,減少了執行緒建立和銷燬的過程,極大的提高了效能。但是它並不能消除上下文切換所帶來的開銷,隨著專案中執行緒資料量的增多,這種切換所帶來的開銷非常嚴重。
7.2 EventLoop介面
EventLoop通俗的理解就是:事件迴圈。直觀的程式碼邏輯:
while (!terminated) { List<Runnable> readyEvents = blockUntilEventsReady(); for (Runnable ev: readyEvents) { ev.run(); } }
Netty提供的EventLoop結合了JDK的併發程式設計和Channel的事件,類層次如下
約定俗成的關係(非常重要):
- 一個EventLoopGroup包含一個或多個EventLoop
- 一個EventLoop在其生命週期內只能和一個Thread繫結
- 所有I/O操作和事件都分配給EventLoop繫結的Thread處理
- 一個Channel在其生命週期內,只能註冊於一個EventLoop
- 一個EventLoop可能被分配處理多個Channel。也就是EventLoop與Channel是1:n的關係
- 一個Channel上的所有ChannelHandler的事件由繫結的EventLoop中的I/O執行緒處理
- 不要阻塞Channel的I/O執行緒,可能會影響該EventLoop中其他Channel事件處理
- EventLoop介面中只定義一個方法: parent() ----- 用於返回當前EventLoop實現的例項所屬的EventLoopGroup的引用
7.2.2 Netty3與Netty4中I/O操作區別
- Netty3中不保證多個執行緒不會在同一時刻訪問出站事件 ----- 不同的執行緒掉用Channel.write()方法,同一個Channel可能同時觸發出站事件。
- Netty4中所有的出站事件肯定是交由EventLoop繫結的執行緒非同步處理,所以不會存在Netty3多個執行緒訪問一個Channel的問題
- Netty3中exceptionCaught事件是一個入站事件,可能在Channel.write()出站事件發生時,發生異常,產生一個exceptonCaught入站事件,則會將異常交由I/O執行緒處理,存在上下文切換
- Metty4則保證Channel上所產生的所有I/O事件,都交由某個指定的EventLoop來處理
7.3 任務排程
排程一個任務以便稍後(延遲)執行或者週期性地執行
7.3.1 JDK的任務排程API
JDK提供Timer定時器和ScheduledExecutorService來實現排程功能。推薦使用ScheduledExecutorService。
ScheduledExecutorService與Timer定時器比較:
- Timer執行週期任務嚴格依賴於系統時間。比如,每5秒執行一次定時任務,當前時間是:2018-08-22 09:00:00,如果將機器時間調成昨天時間2018-08-21。那麼Timer定時任務將失效。而ScheduledExecutorService依然有效,其基於時間的延遲,與系統時間改變無關
- Timer是單執行緒,當執行多個任務時,任務1丟擲異常並且為正常處理,Timer執行緒將退出。所有的任務都不再被排程。而ScheduledExecutorService則保證task1出現異常時,不影響task2的執行
- Timer是單執行緒,如果task1執行非常耗時,則會影響task2執行。而ScheduledExecutorService則可以是多執行緒處理
7.3.2 使用EventLoop排程任務
Netty提供的EventLoop能夠幫助使用者實現週期性任務排程任務。從圖7-2中可以發現,EventLoop擴充套件了ScheduledExecutorService。
7.4 實現細節
7.4.1 執行緒管理
Netty執行緒模型的卓越效能取決於對於當前執行的Thread的身份的確定,通過呼叫EventExecutor的inEventLoop(Thread)方法實現。EventLoop會做以下判斷,以提交處理效能,減少執行緒切換代價
- 如果當前呼叫執行緒正是支援EventLoop的執行緒,那麼提交的程式碼塊將會被(直接)執行
- 否則,EventLoop將它放入到內部佇列中,以便稍後執行
- 注意:每個EventLoop都有自己的任務佇列,獨立於其他的EventLoop
下圖是Netty執行緒模型的關鍵組成部分[通過閱讀原始碼4.1.22發現,實現邏輯並非下圖,待考究]
7.4.2 EventLoop執行緒的分配
根據不同的傳輸實現,EventLoop的建立和分配方式也不同
7.4.2.1 非同步傳輸
非同步傳輸通過儘可能少量的Thread來支援大量的Channel,而不是每個Channel分配一個Thread。少量的EventLoop可能會被多個Channel共享。
下圖3個固定大小的EventLoop(每個EventLoop都有一個Thread支撐),支援多個Channel的所有事件和任務
- EventLoopGroup負責為每個新建立的Channel分配一個EventLoop。當前實現採用順序迴圈(round-robin)方式來進行分配
- 一旦一個Channel被分配給一個EventLoop,那麼它的整個生命週期中都使用這個EventLoop
- 提醒:由於EventLoop與Channel的對映關係是1:n,所以當使用ThreadLocal時,請開發人員知曉其中利弊。
7.4.2.2 阻塞傳輸
用於像OIO這樣的其他傳輸設計,每個Channel都將被分配給一個EventLoop(以及它的Thread),EventLoop與Channel的對映關係是1:1