摘要:目錄源碼之下無秘密做最好的源碼分析教程源碼分析之番外篇的前生今世的前生今世之一簡介的前生今世之二小結的前生今世之三詳解的前生今世之四詳解源碼分析之零磨刀不誤砍柴工源碼分析環境搭建源碼分析之一揭開神秘的紅蓋頭源碼分析之一揭開神秘的紅蓋頭客戶端
目錄
源碼之下無秘密 ── 做最好的 Netty 源碼分析教程
Netty 源碼分析之 番外篇 Java NIO 的前生今世
Java NIO 的前生今世 之一 簡介
Java NIO 的前生今世 之二 NIO Channel 小結
Java NIO 的前生今世 之三 NIO Buffer 詳解
Java NIO 的前生今世 之四 NIO Selector 詳解
Netty 源碼分析之 零 磨刀不誤砍柴工 源碼分析環境搭建
Netty 源碼分析之 一 揭開 Bootstrap 神秘的紅蓋頭
Netty 源碼分析之 一 揭開 Bootstrap 神秘的紅蓋頭 (客戶端)
Netty 源碼分析之 一 揭開 Bootstrap 神秘的紅蓋頭 (服務器端)
Netty 源碼分析之 二 貫穿 Netty 的大動脈 ── ChannelPipeline (一)
Netty 源碼分析之 二 貫穿 Netty 的大動脈 ── ChannelPipeline (二)
Netty 源碼分析之 三 我就是大名鼎鼎的 EventLoop(一)
此文章已同步發送到我的 github 上
簡述這一章是 Netty 源碼分析 的第三章, 我將在這一章中大家一起探究一下 Netty 的 EventLoop 的底層原理, 讓大家對 Netty 的線程模型有更加深入的了解.
NioEventLoopGroup在 Netty 源碼分析之 一 揭開 Bootstrap 神秘的紅蓋頭 (客戶端) 章節中我們已經知道了, 一個 Netty 程序啟動時, 至少要指定一個 EventLoopGroup(如果使用到的是 NIO, 那么通常是 NioEventLoopGroup), 那么這個 NioEventLoopGroup 在 Netty 中到底扮演著什么角色呢? 我們知道, Netty 是 Reactor 模型的一個實現, 那么首先從 Reactor 的線程模型開始吧.
關于 Reactor 的線程模型首先我們來看一下 Reactor 的線程模型.
Reactor 的線程模型有三種:
單線程模型
多線程模型
主從多線程模型
首先來看一下 單線程模型:
所謂單線程, 即 acceptor 處理和 handler 處理都在一個線程中處理. 這個模型的壞處顯而易見: 當其中某個 handler 阻塞時, 會導致其他所有的 client 的 handler 都得不到執行, 并且更嚴重的是, handler 的阻塞也會導致整個服務不能接收新的 client 請求(因為 acceptor 也被阻塞了). 因為有這么多的缺陷, 因此單線程Reactor 模型用的比較少.
那么什么是 多線程模型 呢? Reactor 的多線程模型與單線程模型的區別就是 acceptor 是一個多帶帶的線程處理, 并且有一組特定的 NIO 線程來負責各個客戶端連接的 IO 操作. Reactor 多線程模型如下:
Reactor 多線程模型 有如下特點:
有專門一個線程, 即 Acceptor 線程用于監聽客戶端的TCP連接請求.
客戶端連接的 IO 操作都是由一個特定的 NIO 線程池負責. 每個客戶端連接都與一個特定的 NIO 線程綁定, 因此在這個客戶端連接中的所有 IO 操作都是在同一個線程中完成的.
客戶端連接有很多, 但是 NIO 線程數是比較少的, 因此一個 NIO 線程可以同時綁定到多個客戶端連接中.
接下來我們再來看一下 Reactor 的主從多線程模型.
一般情況下, Reactor 的多線程模式已經可以很好的工作了, 但是我們考慮一下如下情況: 如果我們的服務器需要同時處理大量的客戶端連接請求或我們需要在客戶端連接時, 進行一些權限的檢查, 那么單線程的 Acceptor 很有可能就處理不過來, 造成了大量的客戶端不能連接到服務器.
Reactor 的主從多線程模型就是在這樣的情況下提出來的, 它的特點是: 服務器端接收客戶端的連接請求不再是一個線程, 而是由一個獨立的線程池組成. 它的線程模型如下:
可以看到, Reactor 的主從多線程模型和 Reactor 多線程模型很類似, 只不過 Reactor 的主從多線程模型的 acceptor 使用了線程池來處理大量的客戶端請求.
NioEventLoopGroup 與 Reactor 線程模型的對應我們介紹了三種 Reactor 的線程模型, 那么它們和 NioEventLoopGroup 又有什么關系呢? 其實, 不同的設置 NioEventLoopGroup 的方式就對應了不同的 Reactor 的線程模型.
單線程模型來看一下下面的例子:
EventLoopGroup bossGroup = new NioEventLoopGroup(1); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup) .channel(NioServerSocketChannel.class) ...
注意, 我們實例化了一個 NioEventLoopGroup, 構造器參數是1, 表示 NioEventLoopGroup 的線程池大小是1. 然后接著我們調用 b.group(bossGroup) 設置了服務器端的 EventLoopGroup. 有些朋友可能會有疑惑: 我記得在啟動服務器端的 Netty 程序時, 是需要設置 bossGroup 和 workerGroup 的, 為什么這里就只有一個 bossGroup?
其實很簡單, ServerBootstrap 重寫了 group 方法:
@Override public ServerBootstrap group(EventLoopGroup group) { return group(group, group); }
因此當傳入一個 group 時, 那么 bossGroup 和 workerGroup 就是同一個 NioEventLoopGroup 了.
這時候呢, 因為 bossGroup 和 workerGroup 就是同一個 NioEventLoopGroup, 并且這個 NioEventLoopGroup 只有一個線程, 這樣就會導致 Netty 中的 acceptor 和后續的所有客戶端連接的 IO 操作都是在一個線程中處理的. 那么對應到 Reactor 的線程模型中, 我們這樣設置 NioEventLoopGroup 時, 就相當于 Reactor 單線程模型.
同理, 再來看一下下面的例子:
EventLoopGroup bossGroup = new NioEventLoopGroup(1); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) ...
bossGroup 中只有一個線程, 而 workerGroup 中的線程是 CPU 核心數乘以2, 因此對應的到 Reactor 線程模型中, 我們知道, 這樣設置的 NioEventLoopGroup 其實就是 Reactor 多線程模型.
主從多線程模型相信讀者朋友都想到了, 實現主從線程模型的例子如下:
EventLoopGroup bossGroup = new NioEventLoopGroup(4); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) ...
bossGroup 線程池中的線程數我們設置為4, 而 workerGroup 中的線程是 CPU 核心數乘以2, 因此對應的到 Reactor 線程模型中, 我們知道, 這樣設置的 NioEventLoopGroup 其實就是 Reactor 主從多線程模型.
根據 @labmem 的提示, Netty 的服務器端的 acceptor 階段, 沒有使用到多線程, 因此上面的 主從多線程模型 在 Netty 的服務器端是不存在的.
服務器端的 ServerSocketChannel 只綁定到了 bossGroup 中的一個線程, 因此在調用 Java NIO 的 Selector.select 處理客戶端的連接請求時, 實際上是在一個線程中的, 所以對只有一個服務的應用來說, bossGroup 設置多個線程是沒有什么作用的, 反而還會造成資源浪費.
經 Google, Netty 中的 bossGroup 為什么使用線程池的原因大家眾所紛紜, 不過我在 stackoverflow 上找到一個比較靠譜的答案:
the creator of Netty says multiple boss threads are useful if we share NioEventLoopGroup between different server bootstraps, but I don"t see the reason for it.
因此上面的 主從多線程模型 分析是有問題, 抱歉.
NioEventLoopGroup 類層次結構 NioEventLoopGroup 實例化過程在前面 Netty 源碼分析之 一 揭開 Bootstrap 神秘的紅蓋頭 (客戶端) 章節中, 我們已經簡單地介紹了一下 NioEventLoopGroup 的初始化過程, 這里再回顧一下:
點此下載原圖
即:
EventLoopGroup(其實是MultithreadEventExecutorGroup) 內部維護一個類型為 EventExecutor children 數組, 其大小是 nThreads, 這樣就構成了一個線程池
如果我們在實例化 NioEventLoopGroup 時, 如果指定線程池大小, 則 nThreads 就是指定的值, 反之是處理器核心數 * 2
MultithreadEventExecutorGroup 中會調用 newChild 抽象方法來初始化 children 數組
抽象方法 newChild 是在 NioEventLoopGroup 中實現的, 它返回一個 NioEventLoop 實例.
NioEventLoop 屬性:
SelectorProvider provider 屬性: NioEventLoopGroup 構造器中通過 SelectorProvider.provider() 獲取一個 SelectorProvider
Selector selector 屬性: NioEventLoop 構造器中通過調用通過 selector = provider.openSelector() 獲取一個 selector 對象.
NioEventLoopNioEventLoop 繼承于 SingleThreadEventLoop, 而 SingleThreadEventLoop 又繼承于 SingleThreadEventExecutor. SingleThreadEventExecutor 是 Netty 中對本地線程的抽象, 它內部有一個 Thread thread 屬性, 存儲了一個本地 Java 線程. 因此我們可以認為, 一個 NioEventLoop 其實和一個特定的線程綁定, 并且在其生命周期內, 綁定的線程都不會再改變.
NioEventLoop 類層次結構NioEventLoop 的類層次結構圖還是比較復雜的, 不過我們只需要關注幾個重要的點即可. 首先 NioEventLoop 的繼承鏈如下:
NioEventLoop -> SingleThreadEventLoop -> SingleThreadEventExecutor -> AbstractScheduledEventExecutor
在 AbstractScheduledEventExecutor 中, Netty 實現了 NioEventLoop 的 schedule 功能, 即我們可以通過調用一個 NioEventLoop 實例的 schedule 方法來運行一些定時任務. 而在 SingleThreadEventLoop 中, 又實現了任務隊列的功能, 通過它, 我們可以調用一個 NioEventLoop 實例的 execute 方法來向任務隊列中添加一個 task, 并由 NioEventLoop 進行調度執行.
通常來說, NioEventLoop 肩負著兩種任務, 第一個是作為 IO 線程, 執行與 Channel 相關的 IO 操作, 包括 調用 select 等待就緒的 IO 事件、讀寫數據與數據的處理等; 而第二個任務是作為任務隊列, 執行 taskQueue 中的任務, 例如用戶調用 eventLoop.schedule 提交的定時任務也是這個線程執行的.
NioEventLoop 的實例化過程點此下載原圖
從上圖可以看到, SingleThreadEventExecutor 有一個名為 thread 的 Thread 類型字段, 這個字段就代表了與 SingleThreadEventExecutor 關聯的本地線程.
下面是這個構造器的代碼:
protected SingleThreadEventExecutor( EventExecutorGroup parent, ThreadFactory threadFactory, boolean addTaskWakesUp) { this.parent = parent; this.addTaskWakesUp = addTaskWakesUp; thread = threadFactory.newThread(new Runnable() { @Override public void run() { boolean success = false; updateLastExecutionTime(); try { SingleThreadEventExecutor.this.run(); success = true; } catch (Throwable t) { logger.warn("Unexpected exception from an event executor: ", t); } finally { // 省略清理代碼 ... } } }); threadProperties = new DefaultThreadProperties(thread); taskQueue = newTaskQueue(); }
在 SingleThreadEventExecutor 構造器中, 通過 threadFactory.newThread 創建了一個新的 Java 線程. 在這個線程中所做的事情主要就是調用 SingleThreadEventExecutor.this.run() 方法, 而因為 NioEventLoop 實現了這個方法, 因此根據多態性, 其實調用的是 NioEventLoop.run() 方法.
EventLoop 與 Channel 的關聯Netty 中, 每個 Channel 都有且僅有一個 EventLoop 與之關聯, 它們的關聯過程如下:
點此下載原圖
從上圖中我們可以看到, 當調用了 AbstractChannel#AbstractUnsafe.register 后, 就完成了 Channel 和 EventLoop 的關聯. register 實現如下:
@Override public final void register(EventLoop eventLoop, final ChannelPromise promise) { // 刪除條件檢查. ... AbstractChannel.this.eventLoop = eventLoop; if (eventLoop.inEventLoop()) { register0(promise); } else { try { eventLoop.execute(new OneTimeTask() { @Override public void run() { register0(promise); } }); } catch (Throwable t) { ... } } }
在 AbstractChannel#AbstractUnsafe.register 中, 會將一個 EventLoop 賦值給 AbstractChannel 內部的 eventLoop 字段, 到這里就完成了 EventLoop 與 Channel 的關聯過程.
EventLoop 的啟動在前面我們已經知道了, NioEventLoop 本身就是一個 SingleThreadEventExecutor, 因此 NioEventLoop 的啟動, 其實就是 NioEventLoop 所綁定的本地 Java 線程的啟動.
依照這個思想, 我們只要找到在哪里調用了 SingleThreadEventExecutor 的 thread 字段的 start() 方法就可以知道是在哪里啟動的這個線程了.
從代碼中搜索, thread.start() 被封裝到 SingleThreadEventExecutor.startThread() 方法中了:
private void startThread() { if (STATE_UPDATER.get(this) == ST_NOT_STARTED) { if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) { thread.start(); } } }
STATE_UPDATER 是 SingleThreadEventExecutor 內部維護的一個屬性, 它的作用是標識當前的 thread 的狀態. 在初始的時候, STATE_UPDATER == ST_NOT_STARTED, 因此第一次調用 startThread() 方法時, 就會進入到 if 語句內, 進而調用到 thread.start().
而這個關鍵的 startThread() 方法又是在哪里調用的呢? 經過方法調用關系搜索, 我們發現, startThread 是在 SingleThreadEventExecutor.execute 方法中調用的:
@Override public void execute(Runnable task) { if (task == null) { throw new NullPointerException("task"); } boolean inEventLoop = inEventLoop(); if (inEventLoop) { addTask(task); } else { startThread(); // 調用 startThread 方法, 啟動EventLoop 線程. addTask(task); if (isShutdown() && removeTask(task)) { reject(); } } if (!addTaskWakesUp && wakesUpForTask(task)) { wakeup(inEventLoop); } }
既然如此, 那現在我們的工作就變為了尋找 在哪里第一次調用了 SingleThreadEventExecutor.execute() 方法.
如果留心的讀者可能已經注意到了, 我們在 EventLoop 與 Channel 的關聯 這一小節時, 有提到到在注冊 channel 的過程中, 會在 AbstractChannel#AbstractUnsafe.register 中調用 eventLoop.execute 方法, 在 EventLoop 中進行 Channel 注冊代碼的執行, AbstractChannel#AbstractUnsafe.register 部分代碼如下:
if (eventLoop.inEventLoop()) { register0(promise); } else { try { eventLoop.execute(new OneTimeTask() { @Override public void run() { register0(promise); } }); } catch (Throwable t) { ... } }
很顯然, 一路從 Bootstrap.bind 方法跟蹤到 AbstractChannel#AbstractUnsafe.register 方法, 整個代碼都是在主線程中運行的, 因此上面的 eventLoop.inEventLoop() 就為 false, 于是進入到 else 分支, 在這個分支中調用了 eventLoop.execute. eventLoop 是一個 NioEventLoop 的實例, 而 NioEventLoop 沒有實現 execute 方法, 因此調用的是 SingleThreadEventExecutor.execute:
@Override public void execute(Runnable task) { ... boolean inEventLoop = inEventLoop(); if (inEventLoop) { addTask(task); } else { startThread(); addTask(task); if (isShutdown() && removeTask(task)) { reject(); } } if (!addTaskWakesUp && wakesUpForTask(task)) { wakeup(inEventLoop); } }
我們已經分析過了, inEventLoop == false, 因此執行到 else 分支, 在這里就調用了 startThread() 方法來啟動 SingleThreadEventExecutor 內部關聯的 Java 本地線程了.
總結一句話, 當 EventLoop.execute 第一次被調用時, 就會觸發 startThread() 的調用, 進而導致了 EventLoop 所對應的 Java 線程的啟動.
我們將 EventLoop 與 Channel 的關聯 小節中的時序圖補全后, 就得到了 EventLoop 啟動過程的時序圖:
點此下載原圖
下一小節: Netty 源碼分析之 三 我就是大名鼎鼎的 EventLoop(二)
本文由 yongshun 發表于個人博客, 采用 署名-相同方式共享 3.0 中國大陸許可協議.
Email: yongshun1228@gmail .com
本文標題為: Netty 源碼分析之 三 我就是大名鼎鼎的 EventLoop(一)
本文鏈接為: https://segmentfault.com/a/1190000007403873
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65282.html
摘要:接上篇源碼分析之三我就是大名鼎鼎的一的處理循環在中一個需要負責兩個工作第一個是作為線程負責相應的操作第二個是作為任務線程執行中的任務接下來我們先從操縱方面入手看一下數據是如何從傳遞到我們的中的是模型的一個實現并且是基于的那么從的前生今世之四 接上篇Netty 源碼分析之 三 我就是大名鼎鼎的 EventLoop(一) Netty 的 IO 處理循環 在 Netty 中, 一個 Even...
摘要:背景在工作中雖然我經常使用到庫但是很多時候對的一些概念還是處于知其然不知其所以然的狀態因此就萌生了學習源碼的想法剛開始看源碼的時候自然是比較痛苦的主要原因有兩個第一網上沒有找到讓我滿意的詳盡的源碼分析的教程第二我也是第一次系統地學習這么大代 背景 在工作中, 雖然我經常使用到 Netty 庫, 但是很多時候對 Netty 的一些概念還是處于知其然, 不知其所以然的狀態, 因此就萌生了學...
摘要:我想這很好的解釋了中,僅僅一個都這么復雜,在單線程或者說串行的程序中,編程往往是很簡單的,說白了就是調用,調用,調用然后返回。 Netty源碼分析(三) 前提概要 這次停更很久了,原因是中途迷茫了一段時間,不過最近調整過來了。不過有點要說下,前幾天和業內某個大佬聊天,收獲很多,所以這篇博文和之前也會不太一樣,我們會先從如果是我自己去實現這個功能需要怎么做開始,然后去看netty源碼,與...
摘要:目錄源碼分析之番外篇的前生今世的前生今世之一簡介的前生今世之二小結的前生今世之三詳解的前生今世之四詳解源碼分析之零磨刀不誤砍柴工源碼分析環境搭建源碼分析之一揭開神秘的紅蓋頭源碼分析之一揭開神秘的紅蓋頭客戶端源碼分析之一揭開神秘的紅蓋頭服務器 目錄 Netty 源碼分析之 番外篇 Java NIO 的前生今世 Java NIO 的前生今世 之一 簡介 Java NIO 的前生今世 ...
摘要:原理剖析第篇之服務端啟動工作原理分析下一大致介紹由于篇幅過長難以發布,所以本章節接著上一節來的,上一章節為原理剖析第篇之服務端啟動工作原理分析上那么本章節就繼續分析的服務端啟動,分析的源碼版本為二三四章節請看上一章節詳見原理剖析第篇之 原理剖析(第 011 篇)Netty之服務端啟動工作原理分析(下) - 一、大致介紹 1、由于篇幅過長難以發布,所以本章節接著上一節來的,上一章節為【原...
閱讀 2733·2021-09-02 15:11
閱讀 905·2019-08-26 18:18
閱讀 1866·2019-08-26 11:57
閱讀 3317·2019-08-23 16:59
閱讀 1993·2019-08-23 16:51
閱讀 2305·2019-08-23 16:11
閱讀 3120·2019-08-23 14:58
閱讀 1106·2019-08-23 11:34