本系列Netty原始碼解析文章基於 4.1.56.Final版本
本文筆者來為大家介紹下Netty的核心引擎Reactor的運轉架構,希望通過本文的介紹能夠讓大家對Reactor是如何驅動著整個Netty框架的運轉有一個全面的認識。也為我們後續進一步介紹Netty關於處理網路請求的整個生命週期的相關內容做一個前置知識的鋪墊,方便大家後續理解。
那麼在開始本文正式的內容之前,筆者先來帶著大家回顧下前邊文章介紹的關於Netty整個框架如何搭建的相關內容,沒有看過筆者前邊幾篇文章的讀者朋友也沒關係,這些並不會影響到本文的閱讀,只不過涉及到相關細節的部分,大家可以在回看下。
前文回顧
在《聊聊Netty那些事兒之Reactor在Netty中的實現(建立篇)》一文中,我們介紹了Netty服務端的核心引擎主從Reactor執行緒組
的建立過程以及相關核心元件裡的重要屬性。在這個過程中,我們還提到了Netty對各種細節進行的優化,比如針對JDK NIO 原生Selector做的一些優化,展現了Netty對效能極致的追求。最終我們建立出瞭如下結構的Reactor。
在上篇文章《詳細圖解Netty Reactor啟動全流程》中,我們完整地介紹了Netty服務端啟動的整個流程,並介紹了在啟動過程中涉及到的ServerBootstrap相關的屬性以及配置方式。用於接收連線的服務端NioServerSocketChannel的建立和初始化過程以及其類的繼承結構。其中重點介紹了NioServerSocketChannel向Reactor的註冊過程以及Reactor執行緒的啟動時機和pipeline的初始化時機。最後介紹了NioServerSocketChannel繫結埠地址的整個流程。在這個過程中我們瞭解了Netty的這些核心元件是如何串聯起來的。
當Netty啟動完畢後,我們得到了如下的框架結構:
主Reactor執行緒組中管理的是NioServerSocketChannel
用於接收客戶端連線,並在自己的pipeline中的ServerBootstrapAcceptor
裡初始化接收到的客戶端連線,隨後會將初始化好的客戶端連線註冊到從Reactor執行緒組中。
從Reactor執行緒組主要負責監聽處理註冊其上的所有客戶端連線的IO就緒事件。
其中一個Channel只能分配給一個固定的Reactor。一個Reactor負責處理多個Channel上的IO就緒事件,這樣可以將服務端承載的全量客戶端連線
分攤到多個Reactor
中處理,同時也能保證Channel上IO處理的執行緒安全性
。Reactor與Channel之間的對應關係如下圖所示:
以上內容就是對筆者前邊幾篇文章的相關內容回顧,大家能回憶起來更好,回憶不起來也沒關係,一點也不影響大家理解本文的內容。如果對相關細節感興趣的同學,可以在閱讀完本文之後,在去回看下。
我們言歸正傳,正式開始本文的內容,筆者接下來會為大家介紹這些核心元件是如何相互配合從而驅動著整個Netty Reactor框架運轉的。
當Netty Reactor框架啟動完畢後,接下來第一件事情也是最重要的事情就是如何來高效的接收客戶端的連線。
那麼在探討Netty服務端如何接收連線之前,我們需要弄清楚Reactor執行緒
的執行機制,它是如何監聽並處理Channel
上的IO就緒事件
的。
本文相當於是後續我們介紹Reactor執行緒
監聽處理ACCEPT事件
,Read事件
,Write事件
的前置篇,本文專注於講述Reactor執行緒
的整個執行框架。理解了本文的內容,對理解後面Reactor執行緒
如何處理IO事件
會大有幫助。
我們在Netty框架的建立階段
和啟動階段
無數次的提到了Reactor執行緒
,那麼在本文要介紹的執行階段
就該這個Reactor執行緒
來大顯神威了。
經過前邊文章的介紹,我們瞭解到Netty中的Reactor執行緒
主要幹三件事情:
-
輪詢註冊在
Reactor
上的所有Channel
感興趣的IO就緒事件
。 -
處理
Channel
上的IO就緒事件
。 -
執行Netty中的非同步任務。
正是這三個部分組成了Reactor
的執行框架,那麼我們現在來看下這個執行框架具體是怎麼運轉的~~
Reactor執行緒的整個執行框架
大家還記不記得筆者在《聊聊Netty那些事兒之從核心角度看IO模型》一文中提到的,IO模型的演變
是圍繞著"如何用盡可能少的執行緒去管理儘可能多的連線"
這一主題進行的。
Netty的IO模型
是通過JDK NIO Selector
實現的IO多路複用模型
,而Netty的IO執行緒模型
為主從Reactor執行緒模型
。
根據《聊聊Netty那些事兒之從核心角度看IO模型》一文中介紹的IO多路複用模型
我們很容易就能理解到Netty會使用一個使用者態的Reactor執行緒
去不斷的通過Selector
在核心態去輪訓Channel
上的IO就緒事件
。
說白了Reactor執行緒
其實執行的就是一個死迴圈
,在死迴圈
中不斷的通過Selector
去輪訓IO就緒事件
,如果發生IO就緒事件
則從Selector
系統呼叫中返回並處理IO就緒事件
,如果沒有發生IO就緒事件
則一直阻塞
在Selector
系統呼叫上,直到滿足Selector喚醒條件
。
以下三個條件中只要滿足任意一個條件,Reactor執行緒就會被從Selector上喚醒:
-
當Selector輪詢到有IO活躍事件發生時。
-
當Reactor執行緒需要執行的
定時任務
到達任務執行時間deadline
時。 -
當有
非同步任務
提交給Reactor時,Reactor執行緒需要從Selector
上被喚醒,這樣才能及時的去執行非同步任務
。
這裡可以看出Netty對
Reactor執行緒
的壓榨還是比較狠的,反正現在也沒有IO就緒事件
需要去處理,不能讓Reactor執行緒
在這裡白白等著,要立即喚醒它,轉去處理提交過來的非同步任務以及定時任務。Reactor執行緒
堪稱996典範
一刻不停歇地運作著。
在瞭解了Reactor執行緒
的大概執行框架後,我們接下來就到原始碼中去看下它的核心運轉框架是如何實現出來的。
由於這塊原始碼比較龐大繁雜,所以筆者先把它的執行框架提取出來,方便大家整體的理解整個執行過程的全貌。
上圖所展示的就是Reactor整個工作體系的全貌,主要分為如下幾個重要的工作模組:
-
Reactor執行緒在Selector上阻塞獲取IO就緒事件。在這個模組中首先會去檢查當前是否有非同步任務需要執行,如果有非同步需要執行,那麼不管當前有沒有IO就緒事件都不能阻塞在Selector上,隨後會去非阻塞的輪詢一下Selector上是否有IO就緒事件,如果有,正好可以和非同步任務一起執行。優先處理IO就緒事件,在執行非同步任務。
-
如果當前沒有非同步任務需要執行,那麼Reactor執行緒會接著檢視是否有定時任務需要執行,如果有則在Selector上阻塞直到定時任務的到期時間deadline,或者滿足其他喚醒條件被喚醒。如果沒有定時任務需要執行,Reactor執行緒則會在Selector上一直阻塞直到滿足喚醒條件。
-
當Reactor執行緒滿足喚醒條件被喚醒後,首先會去判斷當前是因為有IO就緒事件被喚醒還是因為有非同步任務需要執行被喚醒或者是兩者都有。隨後Reactor執行緒就會去處理IO就緒事件和執行非同步任務。
-
最後Reactor執行緒返回迴圈起點不斷的重複上述三個步驟。
以上就是Reactor執行緒執行的整個核心邏輯,下面是筆者根據上述核心邏輯,將Reactor的整體程式碼設計框架提取出來,大家可以結合上邊的Reactor工作流程圖,從總體上先感受下整個原始碼實現框架,能夠把Reactor的核心處理步驟和程式碼中相應的處理模組對應起來即可,這裡不需要讀懂每一行程式碼,要以邏輯處理模組為單位理解。後面筆者會將這些一個一個的邏輯處理模組在單獨拎出來為大家詳細介紹。
@Override
protected void run() {
//記錄輪詢次數 用於解決JDK epoll的空輪訓bug
int selectCnt = 0;
for (;;) {
try {
//輪詢結果
int strategy;
try {
//根據輪詢策略獲取輪詢結果 這裡的hasTasks()主要檢查的是普通佇列和尾部佇列中是否有非同步任務等待執行
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// NIO不支援自旋(BUSY_WAIT)
case SelectStrategy.SELECT:
核心邏輯是有任務需要執行,則Reactor執行緒立馬執行非同步任務,如果沒有非同步任務執行,則進行輪詢IO事件
default:
}
} catch (IOException e) {
................省略...............
}
執行到這裡說明滿足了喚醒條件,Reactor執行緒從selector上被喚醒開始處理IO就緒事件和執行非同步任務
/**
* Reactor執行緒需要保證及時的執行非同步任務,只要有非同步任務提交,就需要退出輪詢。
* 有IO事件就優先處理IO事件,然後處理非同步任務
* */
selectCnt++;
//主要用於從IO就緒的SelectedKeys集合中剔除已經失效的selectKey
needsToSelectAgain = false;
//調整Reactor執行緒執行IO事件和執行非同步任務的CPU時間比例 預設50,表示執行IO事件和非同步任務的時間比例是一比一
final int ioRatio = this.ioRatio;
這裡主要處理IO就緒事件,以及執行非同步任務
需要優先處理IO就緒事件,然後根據ioRatio設定的處理IO事件CPU用時與非同步任務CPU用時比例,
來決定執行多長時間的非同步任務
//判斷是否觸發JDK Epoll BUG 觸發空輪詢
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;
} else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
//既沒有IO就緒事件,也沒有非同步任務,Reactor執行緒從Selector上被異常喚醒 觸發JDK Epoll空輪訓BUG
//重新構建Selector,selectCnt歸零
selectCnt = 0;
}
} catch (CancelledKeyException e) {
................省略...............
} catch (Error e) {
................省略...............
} catch (Throwable t) {
................省略...............
} finally {
................省略...............
}
}
}
從上面提取出來的Reactor的原始碼實現框架中,我們可以看出Reactor執行緒
主要做了下面幾個事情:
- 通過
JDK NIO Selector
輪詢註冊在Reactor
上的所有Channel
感興趣的IO事件
。對於NioServerSocketChannel來說因為它主要負責接收客戶端連線所以監聽的是OP_ACCEPT事件
,對於客戶端NioSocketChannel來說因為它主要負責處理連線上的讀寫事件所以監聽的是OP_READ
和OP_WRITE
事件。
這裡需要注意的是netty只會自動註冊
OP_READ
事件,而OP_WRITE事件
是在當Socket寫入緩衝區以滿無法繼續寫入傳送資料時由使用者自己註冊。
-
如果有非同步任務需要執行,則立馬停止輪詢操作,轉去執行非同步任務。這裡分為兩種情況:
-
既有
IO就緒事件
發生,也有非同步任務
需要執行。則優先處理IO就緒事件
,然後根據ioRatio
設定的執行時間比例
決定執行多長時間的非同步任務。這裡Reactor執行緒需要控制非同步任務的執行時間,因為Reactor執行緒的核心是處理IO就緒事件,不能因為非同步任務的執行而耽誤了最重要的事情。 -
沒有
IO就緒事件
發生,但是有非同步任務或者定時任務到期需要執行。則只執行非同步任務
,儘可能的去壓榨Reactor執行緒。沒有IO就緒事件發生也不能閒著。
這裡第二種情況下只會執行
64
個非同步任務,目的是為了防止過度
執行非同步任務,耽誤了
最重要的事情輪詢IO事件
。 -
-
在最後Netty會判斷本次
Reactor執行緒
的喚醒是否是由於觸發了JDK epoll 空輪詢 BUG導致的,如果觸發了該BUG,則重建Selector
。繞過JDK BUG,達到解決問題的目的。
正常情況下Reactor執行緒從Selector中被喚醒有兩種情況:
- 輪詢到有IO就緒事件發生。
- 有非同步任務或者定時任務需要執行。
而JDK epoll 空輪詢 BUG會在上述兩種情況都沒有發生的時候,Reactor執行緒
會意外的從Selector
中被喚醒,導致CPU空轉。
JDK epoll 空輪詢 BUG:https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6670302
好了,Reactor執行緒
的總體執行結構框架我們現在已經瞭解了,下面我們來深入到這些核心處理模組中來各個擊破它們~~
1. Reactor執行緒輪詢IO就緒事件
在《聊聊Netty那些事兒之Reactor在Netty中的實現(建立篇)》一文中,筆者在講述主從Reactor執行緒組NioEventLoopGroup
的建立過程的時候,提到一個構造器引數SelectStrategyFactory
。
public NioEventLoopGroup(
int nThreads, Executor executor, final SelectorProvider selectorProvider) {
this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);
}
public NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,
final SelectStrategyFactory selectStrategyFactory) {
super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());
}
Reactor執行緒
最重要的一件事情就是輪詢IO就緒事件
,SelectStrategyFactory
就是用於指定輪詢策略的,預設實現為DefaultSelectStrategyFactory.INSTANCE
。
而在Reactor執行緒
開啟輪詢的一開始,就是用這個selectStrategy
去計算一個輪詢策略strategy
,後續會根據這個strategy
進行不同的邏輯處理。
@Override
protected void run() {
//記錄輪詢次數 用於解決JDK epoll的空輪訓bug
int selectCnt = 0;
for (;;) {
try {
//輪詢結果
int strategy;
try {
//根據輪詢策略獲取輪詢結果 這裡的hasTasks()主要檢查的是普通佇列和尾部佇列中是否有非同步任務等待執行
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// NIO不支援自旋(BUSY_WAIT)
case SelectStrategy.SELECT:
核心邏輯是有任務需要執行,則Reactor執行緒立馬執行非同步任務,如果沒有非同步任務執行,則進行輪詢IO事件
default:
}
} catch (IOException e) {
................省略...............
}
................省略...............
}
下面我們來看這個輪詢策略strategy
具體的計算邏輯是什麼樣的?
1.1 輪詢策略
public interface SelectStrategy {
/**
* Indicates a blocking select should follow.
*/
int SELECT = -1;
/**
* Indicates the IO loop should be retried, no blocking select to follow directly.
*/
int CONTINUE = -2;
/**
* Indicates the IO loop to poll for new events without blocking.
*/
int BUSY_WAIT = -3;
int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception;
}
我們首先來看下Netty中定義的這三種輪詢策略:
-
SelectStrategy.SELECT:
此時沒有任何非同步任務需要執行,Reactor執行緒
可以安心的阻塞
在Selector
上等待IO就緒事件
的來臨。 -
SelectStrategy.CONTINUE:
重新開啟一輪IO輪詢
。 -
SelectStrategy.BUSY_WAIT:
Reactor執行緒進行自旋輪詢
,由於NIO 不支援自旋操作
,所以這裡直接跳到SelectStrategy.SELECT
策略。
下面我們來看下輪詢策略
的計算邏輯calculateStrategy
:
final class DefaultSelectStrategy implements SelectStrategy {
static final SelectStrategy INSTANCE = new DefaultSelectStrategy();
private DefaultSelectStrategy() { }
@Override
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
/**
* Reactor執行緒要保證及時的執行非同步任務
* 1:如果有非同步任務等待執行,則馬上執行selectNow()非阻塞輪詢一次IO就緒事件
* 2:沒有非同步任務,則跳到switch select分支
* */
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}
}
- 在
Reactor執行緒
的輪詢工作開始之前,需要首先判斷下當前是否有非同步任務
需要執行。判斷依據就是檢視Reactor
中的非同步任務佇列taskQueue
和用於統計資訊任務用的尾部佇列tailTask
是否有非同步任務
。
@Override
protected boolean hasTasks() {
return super.hasTasks() || !tailTasks.isEmpty();
}
protected boolean hasTasks() {
assert inEventLoop();
return !taskQueue.isEmpty();
}
- 如果
Reactor
中有非同步任務
需要執行,那麼Reactor執行緒
需要立即執行,不能阻塞在Selector
上。在返回前需要再順帶呼叫selectNow()
非阻塞檢視一下當前是否有IO就緒事件
發生。如果有,那麼正好可以和非同步任務
一起被處理,如果沒有,則及時地處理非同步任務
。
這裡Netty要表達的語義是:首先Reactor執行緒需要優先保證
IO就緒事件
的處理,然後在保證非同步任務
的及時執行。如果當前沒有IO就緒事件但是有非同步任務需要執行時,Reactor執行緒就要去及時執行非同步任務而不是繼續阻塞在Selector上等待IO就緒事件。
private final IntSupplier selectNowSupplier = new IntSupplier() {
@Override
public int get() throws Exception {
return selectNow();
}
};
int selectNow() throws IOException {
//非阻塞
return selector.selectNow();
}
- 如果當前
Reactor執行緒
沒有非同步任務需要執行,那麼calculateStrategy
方法直接返回SelectStrategy.SELECT
也就是SelectStrategy介面
中定義的常量-1
。當calculateStrategy
方法通過selectNow()
返回非零
數值時,表示此時有IO就緒
的Channel
,返回的數值表示有多少個IO就緒
的Channel
。
@Override
protected void run() {
//記錄輪詢次數 用於解決JDK epoll的空輪訓bug
int selectCnt = 0;
for (;;) {
try {
//輪詢結果
int strategy;
try {
//根據輪詢策略獲取輪詢結果 這裡的hasTasks()主要檢查的是普通佇列和尾部佇列中是否有非同步任務等待執行
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// NIO不支援自旋(BUSY_WAIT)
case SelectStrategy.SELECT:
核心邏輯是有任務需要執行,則Reactor執行緒立馬執行非同步任務,如果沒有非同步任務執行,則進行輪詢IO事件
default:
}
} catch (IOException e) {
................省略...............
}
................處理IO就緒事件以及執行非同步任務...............
}
從預設的輪詢策略我們可以看出selectStrategy.calculateStrategy
只會返回三種情況:
-
返回 -1:
switch邏輯分支進入SelectStrategy.SELECT分支
,表示此時Reactor
中沒有非同步任務
需要執行,Reactor執行緒
可以安心的阻塞在Selector
上等待IO就緒事件
發生。 -
返回 0:
switch邏輯分支進入default分支
,表示此時Reactor
中沒有IO就緒事件
但是有非同步任務
需要執行,流程通過default分支
直接進入了處理非同步任務
的邏輯部分。 -
返回 > 0:
switch邏輯分支進入default分支
,表示此時Reactor
中既有IO就緒事件
發生也有非同步任務
需要執行,流程通過default分支
直接進入了處理IO就緒事件
和執行非同步任務
邏輯部分。
現在Reactor
的流程處理邏輯走向我們清楚了,那麼接下來我們把重點放在SelectStrategy.SELECT分支中的輪詢邏輯上。這塊是Reactor監聽IO就緒事件的核心。
1.2 輪詢邏輯
case SelectStrategy.SELECT:
//當前沒有非同步任務執行,Reactor執行緒可以放心的阻塞等待IO就緒事件
//從定時任務佇列中取出即將快要執行的定時任務deadline
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
// -1代表當前定時任務佇列中沒有定時任務
curDeadlineNanos = NONE; // nothing on the calendar
}
//最早執行定時任務的deadline作為 select的阻塞時間,意思是到了定時任務的執行時間
//不管有無IO就緒事件,必須喚醒selector,從而使reactor執行緒執行定時任務
nextWakeupNanos.set(curDeadlineNanos);
try {
if (!hasTasks()) {
//再次檢查普通任務佇列中是否有非同步任務
//沒有的話開始select阻塞輪詢IO就緒事件
strategy = select(curDeadlineNanos);
}
} finally {
// 執行到這裡說明Reactor已經從Selector上被喚醒了
// 設定Reactor的狀態為甦醒狀態AWAKE
// lazySet優化不必要的volatile操作,不使用記憶體屏障,不保證寫操作的可見性(單執行緒不需要保證)
nextWakeupNanos.lazySet(AWAKE);
}
流程走到這裡,說明現在Reactor
上沒有任何事情可做,可以安心的阻塞
在Selector
上等待IO就緒事件
到來。
那麼Reactor執行緒
到底應該在Selector
上阻塞多久呢??
在回答這個問題之前,我們在回顧下《聊聊Netty那些事兒之Reactor在Netty中的實現(建立篇)》一文中在講述Reactor的建立
時提到,Reactor執行緒
除了要輪詢Channel
上的IO就緒事件
,以及處理IO就緒事件
外,還有一個任務就是負責執行Netty框架中的非同步任務
。
而Netty框架中的非同步任務
分為三類:
-
存放在普通任務佇列
taskQueue
中的普通非同步任務。 -
存放在尾部佇列
tailTasks
中的用於執行統計任務等收尾動作的尾部任務。 -
還有一種就是這裡即將提到的
定時任務
。存放在Reactor
中的定時任務佇列scheduledTaskQueue
中。
從ReactorNioEventLoop類
中的繼承結構我們也可以看出,Reactor
具備執行定時任務的能力。
既然Reactor
需要執行定時任務,那麼它就不能一直阻塞
在Selector
上無限等待IO就緒事件
。
那麼我們回到本小節一開始提到的問題上,為了保證Reactor
能夠及時地執行定時任務
,Reactor執行緒
需要在即將要執行的的第一個定時任務deadline
到達之前被喚醒。
所以在Reactor
執行緒開始輪詢IO就緒事件
之前,我們需要首先計算出來Reactor執行緒
在Selector
上的阻塞超時時間。
1.2.1 Reactor的輪詢超時時間
首先我們需要從Reactor
的定時任務佇列scheduledTaskQueue
中取出即將快要執行的定時任務deadline
。將這個deadline
作為Reactor執行緒
在Selector
上輪詢的超時時間。這樣可以保證在定時任務即將要執行時,Reactor現在可以及時的從Selector上被喚醒。
private static final long AWAKE = -1L;
private static final long NONE = Long.MAX_VALUE;
// nextWakeupNanos is:
// AWAKE when EL is awake
// NONE when EL is waiting with no wakeup scheduled
// other value T when EL is waiting with wakeup scheduled at time T
private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE);
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
// -1代表當前定時任務佇列中沒有定時任務
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue;
protected final long nextScheduledTaskDeadlineNanos() {
ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
return scheduledTask != null ? scheduledTask.deadlineNanos() : -1;
}
final ScheduledFutureTask<?> peekScheduledTask() {
Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
return scheduledTaskQueue != null ? scheduledTaskQueue.peek() : null;
}
}
nextScheduledTaskDeadlineNanos
方法會返回當前Reactor
定時任務佇列中最近的一個定時任務deadline
時間點,如果定時任務佇列中沒有定時任務,則返回-1
。
NioEventLoop
中nextWakeupNanos
變數用來存放Reactor從Selector
上被喚醒的時間點,設定為最近需要被執行定時任務的deadline
,如果當前並沒有定時任務需要執行,那麼就設定為Long.MAX_VALUE
一直阻塞,直到有IO就緒事件
到達或者有非同步任務
需要執行。
1.2.2 Reactor開始輪詢IO就緒事件
if (!hasTasks()) {
//再次檢查普通任務佇列中是否有非同步任務, 沒有的話 開始select阻塞輪詢IO就緒事件
strategy = select(curDeadlineNanos);
}
在Reactor執行緒
開始阻塞
輪詢IO就緒事件
之前還需要再次檢查一下是否有非同步任務
需要執行。
如果此時恰巧有非同步任務
提交,就需要停止IO就緒事件
的輪詢,轉去執行非同步任務
。如果沒有非同步任務
,則正式開始輪詢IO就緒事件
。
private int select(long deadlineNanos) throws IOException {
if (deadlineNanos == NONE) {
//無定時任務,無普通任務執行時,開始輪詢IO就緒事件,沒有就一直阻塞 直到喚醒條件成立
return selector.select();
}
long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}
如果deadlineNanos == NONE
,經過上小節的介紹,我們知道NONE
表示當前Reactor
中並沒有定時任務,所以可以安心的阻塞
在Selector
上等待IO就緒事件
到來。
selector.select()
呼叫是一個阻塞呼叫,如果沒有IO就緒事件
,Reactor執行緒
就會一直阻塞在這裡直到IO就緒事件
到來。這裡佔時不考慮前邊提到的JDK NIO Epoll的空輪詢BUG
.
讀到這裡那麼問題來了,此時Reactor執行緒
正阻塞在selector.select()
呼叫上等待IO就緒事件
的到來,如果此時正好有非同步任務
被提交到Reactor
中需要執行,並且此時無任何IO就緒事件
,而Reactor執行緒
由於沒有IO就緒事件
到來,會繼續在這裡阻塞,那麼如何去執行非同步任務
呢??
解鈴還須繫鈴人,既然非同步任務
在被提交後希望立馬得到執行,那麼就在提交非同步任務
的時候去喚醒Reactor執行緒
。
//addTaskWakesUp = true 表示 當且僅當只有呼叫addTask方法時 才會喚醒Reactor執行緒
//addTaskWakesUp = false 表示 並不是只有addTask方法才能喚醒Reactor 還有其他方法可以喚醒Reactor 預設設定false
private final boolean addTaskWakesUp;
private void execute(Runnable task, boolean immediate) {
boolean inEventLoop = inEventLoop();
addTask(task);
if (!inEventLoop) {
//如果當前執行緒不是Reactor執行緒,則啟動Reactor執行緒
//這裡可以看出Reactor執行緒的啟動是通過 向NioEventLoop新增非同步任務時啟動的
startThread();
.....................省略...................
}
if (!addTaskWakesUp && immediate) {
//io.netty.channel.nio.NioEventLoop.wakeup
wakeup(inEventLoop);
}
}
對於execute方法
我想大家一定不會陌生,在上篇文章《詳細圖解Netty Reactor啟動全流程》中我們在介紹Reactor執行緒的啟動
時介紹過該方法。
在啟動過程中涉及到的重要操作Register操作
,Bind操作
都需要封裝成非同步任務
通過該方法提交到Reactor
中執行。
這裡我們將重點放在execute方法
後半段wakeup
邏輯部分。
我們先介紹下和wakeup
邏輯相關的兩個引數boolean immediate
和boolean addTaskWakesUp
。
-
immediate:
表示提交的task
是否需要被立即執行。Netty中只要你提交的任務型別不是LazyRunnable
型別的任務,都是需要立即執行的。immediate = true
-
addTaskWakesUp :
true
表示當且僅當只有
呼叫addTask
方法時才會喚醒Reactor執行緒
。呼叫別的方法並不會喚醒Reactor執行緒
。
在初始化NioEventLoop
時會設定為false
,表示並不是只有
addTask方法才能喚醒Reactor執行緒
還有其他方法可以喚醒Reactor執行緒
,比如這裡的execute方法
就會喚醒Reactor執行緒
。
針對execute方法中的這個喚醒條件!addTaskWakesUp && immediate
,netty這裡要表達的語義是:當immediate引數為true的時候表示該非同步任務需要立即執行,addTaskWakesUp 預設設定為false 表示不僅只有addTask方法可以喚醒Reactor,還有其他方法比如這裡的execute方法也可以喚醒。但是當設定為true時,語義就變為只有addTask才可以喚醒Reactor,即使execute方法裡的immediate = true也不能喚醒Reactor,因為執行的是execute方法而不是addTask方法。
private static final long AWAKE = -1L;
private final AtomicLong nextWakeupNanos = new AtomicLong(AWAKE);
protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && nextWakeupNanos.getAndSet(AWAKE) != AWAKE) {
//將Reactor執行緒從Selector上喚醒
selector.wakeup();
}
}
當nextWakeupNanos = AWAKE
時表示當前Reactor正處於甦醒狀態,既然是甦醒狀態也就沒有必要去執行 selector.wakeup()
重複喚醒Reactor了,同時也能省去這一次的系統呼叫開銷。
在《1.2小節 輪詢邏輯》開始介紹的原始碼實現框架裡Reactor被喚醒之後執行程式碼會進入finally{...}
語句塊中,在那裡會將nextWakeupNanos
設定為AWAKE
。
try {
if (!hasTasks()) {
strategy = select(curDeadlineNanos);
}
} finally {
// 執行到這裡說明Reactor已經從Selector上被喚醒了
// 設定Reactor的狀態為甦醒狀態AWAKE
// lazySet優化不必要的volatile操作,不使用記憶體屏障,不保證寫操作的可見性(單執行緒不需要保證)
nextWakeupNanos.lazySet(AWAKE);
}
這裡Netty用了一個
AtomicLong型別
的變數nextWakeupNanos
,既能表示當前Reactor執行緒
的狀態,又能表示Reactor執行緒
的阻塞超時時間。我們在日常開發中也可以學習下這種技巧。
我們繼續回到Reactor執行緒
輪詢IO就緒事件
的主線上。
private int select(long deadlineNanos) throws IOException {
if (deadlineNanos == NONE) {
//無定時任務,無普通任務執行時,開始輪詢IO就緒事件,沒有就一直阻塞 直到喚醒條件成立
return selector.select();
}
long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}
當deadlineNanos
不為NONE
,表示此時Reactor
有定時任務
需要執行,Reactor執行緒
需要阻塞在Selector
上等待IO就緒事件
直到最近的一個定時任務執行時間點deadline
到達。
這裡的deadlineNanos
表示的就是Reactor
中最近的一個定時任務執行時間點deadline
,單位是納秒
。指的是一個絕對時間
。
而我們需要計算的是Reactor執行緒
阻塞在Selector
的超時時間timeoutMillis
,單位是毫秒
,指的是一個相對時間
。
所以在Reactor執行緒
開始阻塞在Selector
上之前,我們需要將這個單位為納秒
的絕對時間deadlineNanos
轉化為單位為毫秒
的相對時間timeoutMillis
。
private int select(long deadlineNanos) throws IOException {
if (deadlineNanos == NONE) {
//無定時任務,無普通任務執行時,開始輪詢IO就緒事件,沒有就一直阻塞 直到喚醒條件成立
return selector.select();
}
long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}
這裡大家可能會好奇,通過deadlineToDelayNanos方法
計算timeoutMillis
的時候,為什麼要給deadlineNanos
在加上0.995毫秒
呢??
大家想象一下這樣的場景,當最近的一個定時任務的deadline
即將在5微秒
內到達,那麼這時將納秒轉換成毫秒計算出的timeoutMillis
會是0
。
而在Netty中timeoutMillis = 0
要表達的語義是:定時任務執行時間已經到達deadline
時間點,需要被執行。
而現實情況是定時任務還有5微秒
才能夠到達deadline
,所以對於這種情況,需要在deadlineNanos
在加上0.995毫秒
湊成1毫秒
不能讓其為0。
所以從這裡我們可以看出,
Reactor
在有定時任務的情況下,至少要阻塞1毫秒
。
public abstract class AbstractScheduledEventExecutor extends AbstractEventExecutor {
protected static long deadlineToDelayNanos(long deadlineNanos) {
return ScheduledFutureTask.deadlineToDelayNanos(deadlineNanos);
}
}
final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFuture<V>, PriorityQueueNode {
static long deadlineToDelayNanos(long deadlineNanos) {
return deadlineNanos == 0L ? 0L : Math.max(0L, deadlineNanos - nanoTime());
}
//啟動時間點
private static final long START_TIME = System.nanoTime();
static long nanoTime() {
return System.nanoTime() - START_TIME;
}
static long deadlineNanos(long delay) {
//計算定時任務執行deadline 去除啟動時間
long deadlineNanos = nanoTime() + delay;
// Guard against overflow
return deadlineNanos < 0 ? Long.MAX_VALUE : deadlineNanos;
}
}
這裡需要注意一下,在建立定時任務時會通過deadlineNanos方法
計算定時任務的執行deadline
,deadline
的計算邏輯是當前時間點
+任務延時delay
-系統啟動時間
。這裡需要扣除系統啟動的時間。
所以這裡在通過deadline
計算延時delay
(也就是timeout)的時候需要在加上系統啟動的時間
: deadlineNanos - nanoTime()
當通過deadlineToDelayNanos
計算出的timeoutMillis <= 0
時,表示Reactor
目前有臨近的定時任務
需要執行,這時候就需要立馬返回,不能阻塞在Selector
上影響定時任務
的執行。當然在返回執行定時任務
前,需要在順手通過selector.selectNow()
非阻塞輪詢一下Channel
上是否有IO就緒事件
到達,防止耽誤IO事件
的處理。真是操碎了心~~
當timeoutMillis > 0
時,Reactor執行緒
就可以安心的阻塞在Selector
上等待IO事件
的到來,直到timeoutMillis
超時時間到達。
timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis)
當註冊在Reactor
上的Channel
中有IO事件
到來時,Reactor執行緒
就會從selector.select(timeoutMillis)
呼叫中喚醒,立即去處理IO就緒事件
。
這裡假設一種極端情況,如果最近的一個定時任務的deadline是在未來很遠的一個時間點,這樣就會使timeoutMillis的時間非常非常久,那麼Reactor豈不是會一直阻塞在Selector上造成 Netty 無法工作?
筆者覺得大家現在心裡應該已經有了答案,我們在《1.2.2 Reactor開始輪詢IO就緒事件》小節一開始介紹過,當Reactor正在Selector上阻塞時,如果此時使用者執行緒向Reactor提交了非同步任務,Reactor執行緒會通過execute方法被喚醒。
流程到這裡,Reactor中最重要也是最核心的邏輯:輪詢Channel
上的IO就緒事件
的處理流程我們就講解完了。
當Reactor輪詢到有IO活躍事件或者有非同步任務需要執行時,就會從Selector上被喚醒,下面就到了該介紹Reactor被喚醒之後是如何處理IO就緒事件
以及如何執行非同步任務
的時候了。
Netty畢竟是一個網路框架,所以它會優先去處理Channel
上的IO事件
,基於這個事實,所以Netty不會容忍非同步任務
被無限制的執行從而影響IO吞吐
。
Netty通過ioRatio變數
來調配Reactor執行緒
在處理IO事件
和執行非同步任務
之間的CPU時間
分配比例。
下面我們就來看下這個執行時間比例的分配邏輯是什麼樣的~~~
2. Reactor處理IO與處理非同步任務的時間比例分配
無論什麼時候,當有IO就緒事件
到來時,Reactor
都需要保證IO事件
被及時完整的處理完,而ioRatio
主要限制的是執行非同步任務
所需用時,防止Reactor執行緒
處理非同步任務
時間過長而導致 I/O 事件
得不到及時地處理。
//調整Reactor執行緒執行IO事件和執行非同步任務的CPU時間比例 預設50,表示執行IO事件和非同步任務的時間比例是一比一
final int ioRatio = this.ioRatio;
boolean ranTasks;
if (ioRatio == 100) { //先一股腦執行IO事件,在一股腦執行非同步任務(無時間限制)
try {
if (strategy > 0) {
//如果有IO就緒事件 則處理IO就緒事件
processSelectedKeys();
}
} finally {
// Ensure we always run tasks.
//處理所有非同步任務
ranTasks = runAllTasks();
}
} else if (strategy > 0) {//先執行IO事件 用時ioTime 執行非同步任務只能用時ioTime * (100 - ioRatio) / ioRatio
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
// 限定在超時時間內 處理有限的非同步任務 防止Reactor執行緒處理非同步任務時間過長而導致 I/O 事件阻塞
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else { //沒有IO就緒事件處理,則只執行非同步任務 最多執行64個 防止Reactor執行緒處理非同步任務時間過長而導致 I/O 事件阻塞
ranTasks = runAllTasks(0); // This will run the minimum number of tasks
}
- 當
ioRatio = 100
時,表示無需考慮執行時間的限制,當有IO就緒事件
時(strategy > 0
)Reactor執行緒
需要優先處理IO就緒事件
,處理完IO事件
後,執行所有的非同步任務
包括:普通任務,尾部任務,定時任務。無時間限制。
strategy
的數值表示IO就緒
的Channel
個數。它是前邊介紹的io.netty.channel.nio.NioEventLoop#select
方法的返回值。
- 當
ioRatio
設定的值不為100
時,預設為50
。需要先統計出執行IO事件
的用時ioTime
,根據ioTime * (100 - ioRatio) / ioRatio
計算出,後面執行非同步任務
的限制時間。也就是說Reactor執行緒
需要在這個限定的時間內,執行有限的非同步任務,防止Reactor執行緒
由於處理非同步任務
時間過長而導致I/O 事件
得不到及時地處理。
預設情況下,執行
IO事件
用時和執行非同步任務
用時比例設定的是一比一。
ioRatio
設定的越高,則Reactor執行緒
執行非同步任務的時間佔比越小
。
要想得到Reactor執行緒
執行非同步任務
所需的時間限制,必須知道執行IO事件
的用時ioTime
然後在根據ioRatio
計算出執行非同步任務
的時間限制。
那如果此時並沒有IO就緒事件
需要Reactor執行緒
處理的話,這種情況下我們無法得到ioTime
,那怎麼得到執行非同步任務
的限制時間呢??
在這種特殊情況下,Netty只允許Reactor執行緒
最多執行64
個非同步任務,然後就結束執行。轉去繼續輪訓IO就緒事件
。核心目的還是防止Reactor執行緒
由於處理非同步任務
時間過長而導致I/O 事件
得不到及時地處理。
預設情況下,當
Reactor
有非同步任務
需要處理但是沒有IO就緒事件
時,Netty只會允許Reactor執行緒
執行最多64
個非同步任務。
現在我們對Reactor
處理IO事件
和非同步任務
的整體框架已經瞭解了,下面我們就來分別介紹下Reactor執行緒
在處理IO事件
和非同步任務
的具體邏輯是什麼樣的?
3. Reactor執行緒處理IO就緒事件
//該欄位為持有selector物件selectedKeys的引用,當IO事件就緒時,直接從這裡獲取
private SelectedSelectionKeySet selectedKeys;
private void processSelectedKeys() {
//是否採用netty優化後的selectedKey集合型別 是由變數DISABLE_KEY_SET_OPTIMIZATION決定的 預設為false
if (selectedKeys != null) {
processSelectedKeysOptimized();
} else {
processSelectedKeysPlain(selector.selectedKeys());
}
}
看到這段程式碼大家眼熟嗎??
不知大家還記不記得我們在《聊聊Netty那些事兒之Reactor在Netty中的實現(建立篇)》一文中介紹Reactor NioEventLoop類
在建立Selector
的過程中提到,出於對JDK NIO Selector
中selectedKeys 集合
的插入
和遍歷
操作效能的考慮Netty將自己用陣列實現的SelectedSelectionKeySet 集合
替換掉了JDK NIO Selector
中selectedKeys
的HashSet
實現。
public abstract class SelectorImpl extends AbstractSelector {
// The set of keys with data ready for an operation
// //IO就緒的SelectionKey(裡面包裹著channel)
protected Set<SelectionKey> selectedKeys;
// The set of keys registered with this Selector
//註冊在該Selector上的所有SelectionKey(裡面包裹著channel)
protected HashSet<SelectionKey> keys;
...............省略...................
}
Netty中通過優化開關DISABLE_KEY_SET_OPTIMIZATION
控制是否對JDK NIO Selector
進行優化。預設是需要優化。
在優化開關開啟的情況下,Netty會將建立的SelectedSelectionKeySet 集合
儲存在NioEventLoop
的private SelectedSelectionKeySet selectedKeys
欄位中,方便Reactor執行緒
直接從這裡獲取IO就緒
的SelectionKey
。
在優化開關關閉的情況下,Netty會直接採用JDK NIO Selector
的預設實現。此時NioEventLoop
的selectedKeys
欄位就會為null
。
忘記這段的同學可以在回顧下《聊聊Netty那些事兒之Reactor在Netty中的實現(建立篇)》一文中關於
Reactor
的建立過程。
經過對前邊內容的回顧,我們看到了在Reactor
處理IO就緒事件
的邏輯也分為兩個部分,一個是經過Netty優化的,一個是採用JDK 原生
的。
我們先來看採用JDK 原生
的Selector
的處理方式,理解了這種方式,在看Netty優化的方式會更加容易。
3.1 processSelectedKeysPlain
我們在《聊聊Netty那些事兒之Reactor在Netty中的實現(建立篇)》一文中介紹JDK NIO Selector
的工作過程時講過,當註冊在Selector
上的Channel
發生IO就緒事件
時,Selector
會將IO就緒
的SelectionKey
插入到Set<SelectionKey> selectedKeys
集合中。
這時Reactor執行緒
會從java.nio.channels.Selector#select(long)
呼叫中返回。隨後呼叫java.nio.channels.Selector#selectedKeys
獲取IO就緒
的SelectionKey
集合。
所以Reactor執行緒
在呼叫processSelectedKeysPlain方法
處理IO就緒事件
之前需要呼叫selector.selectedKeys()
去獲取所有IO就緒
的SelectionKeys
。
processSelectedKeysPlain(selector.selectedKeys())
private void processSelectedKeysPlain(Set<SelectionKey> selectedKeys) {
if (selectedKeys.isEmpty()) {
return;
}
Iterator<SelectionKey> i = selectedKeys.iterator();
for (;;) {
final SelectionKey k = i.next();
final Object a = k.attachment();
//注意每次迭代末尾的keyIterator.remove()呼叫。Selector不會自己從已選擇鍵集中移除SelectionKey例項。
//必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
i.remove();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (!i.hasNext()) {
break;
}
//目的是再次進入for迴圈 移除失效的selectKey(socketChannel可能從selector上移除)
if (needsToSelectAgain) {
selectAgain();
selectedKeys = selector.selectedKeys();
// Create the iterator again to avoid ConcurrentModificationException
if (selectedKeys.isEmpty()) {
break;
} else {
i = selectedKeys.iterator();
}
}
}
}
3.1.1 獲取IO就緒的Channel
Set<SelectionKey> selectedKeys
集合裡面裝的全部是IO就緒
的SelectionKey
,注意,此時Set<SelectionKey> selectedKeys
的實現型別為HashSet型別
。因為我們這裡首先介紹的是JDK NIO 原生實現。
通過獲取HashSet
的迭代器,開始逐個處理IO就緒
的Channel
。
Iterator<SelectionKey> i = selectedKeys.iterator();
final SelectionKey k = i.next();
final Object a = k.attachment();
大家還記得這個SelectionKey
中的attachment屬性
裡存放的是什麼嗎??
在上篇文章《詳細圖解Netty Reactor啟動全流程》中我們在講NioServerSocketChannel
向Main Reactor
註冊的時候,通過this指標將自己作為SelectionKey
的attachment屬性
註冊到Selector
中。這一步完成了Netty自定義Channel
和JDK NIO Channel
的繫結。
public abstract class AbstractNioChannel extends AbstractChannel {
//channel註冊到Selector後獲得的SelectKey
volatile SelectionKey selectionKey;
@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
...............省略....................
}
}
}
}
而我們也提到SelectionKey
就相當於是Channel
在Selector
中的一種表示,當Channel
上有IO就緒事件
時,Selector
會將Channel
對應的SelectionKey
返回給Reactor執行緒
,我們可以通過返回的這個SelectionKey
裡的attachment屬性
獲取到對應的Netty自定義Channel
。
對於客戶端連線事件(
OP_ACCEPT
)活躍時,這裡的Channel型別
為NioServerSocketChannel
。
對於客戶端讀寫事件(Read
,Write
)活躍時,這裡的Channel型別
為NioSocketChannel
。
當我們通過k.attachment()
獲取到Netty自定義的Channel
時,就需要把這個Channel
對應的SelectionKey
從Selector
的就緒集合Set<SelectionKey> selectedKeys
中刪除。因為Selector自己不會主動刪除已經處理完的SelectionKey,需要呼叫者自己主動刪除,這樣當這個Channel
再次IO就緒時
,Selector
會再次將這個Channel
對應的SelectionKey
放入就緒集合Set<SelectionKey> selectedKeys
中。
i.remove();
3.1.2 處理Channel上的IO事件
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
從這裡我們可以看出Netty向SelectionKey
中的attachment屬性
附加的物件分為兩種:
-
一種是我們熟悉的
Channel
,無論是服務端使用的NioServerSocketChannel
還是客戶端使用的NioSocketChannel
都屬於AbstractNioChannel
。Channel
上的IO事件
是由Netty框架負責處理,也是本小節我們要重點介紹的 -
另一種就是
NioTask
,這種型別是Netty提供給使用者可以自定義一些當Channel
上發生IO就緒事件
時的自定義處理。
public interface NioTask<C extends SelectableChannel> {
/**
* Invoked when the {@link SelectableChannel} has been selected by the {@link Selector}.
*/
void channelReady(C ch, SelectionKey key) throws Exception;
/**
* Invoked when the {@link SelectionKey} of the specified {@link SelectableChannel} has been cancelled and thus
* this {@link NioTask} will not be notified anymore.
*
* @param cause the cause of the unregistration. {@code null} if a user called {@link SelectionKey#cancel()} or
* the event loop has been shut down.
*/
void channelUnregistered(C ch, Throwable cause) throws Exception;
}
NioTask
和Channel
其實本質上是一樣的都是負責處理Channel
上的IO就緒事件
,只不過一個是使用者自定義處理
,一個是Netty框架處理。這裡我們重點關注Channel
的IO處理邏輯
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
//獲取Channel的底層操作類Unsafe
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
......如果SelectionKey已經失效則關閉對應的Channel......
}
try {
//獲取IO就緒事件
int readyOps = k.readyOps();
//處理Connect事件
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
//移除對Connect事件的監聽,否則Selector會一直通知
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
//觸發channelActive事件處理Connect事件
unsafe.finishConnect();
}
//處理Write事件
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
//處理Read事件或者Accept事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
- 首先我們需要獲取
IO就緒Channel
底層的操作類Unsafe
,用於對具體IO就緒事件
的處理。
這裡可以看出,Netty對
IO就緒事件
的處理全部封裝在Unsafe類
中。比如:對OP_ACCEPT事件
的具體處理邏輯是封裝在NioServerSocketChannel
中的UnSafe類
中。對OP_READ或者OP_WRITE事件
的處理是封裝在NioSocketChannel
中的Unsafe類
中。
- 從
Selectionkey
中獲取具體IO就緒事件 readyOps
。
SelectonKey
中關於IO事件
的集合有兩個。一個是interestOps
,用於記錄Channel
感興趣的IO事件
,在Channel
向Selector
註冊完畢後,通過pipeline
中的HeadContext
節點的ChannelActive事件回撥
中新增。下面這段程式碼就是在ChannelActive事件回撥
中Channel在向Selector註冊自己感興趣的IO事件。
public abstract class AbstractNioChannel extends AbstractChannel {
@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
readPending = true;
final int interestOps = selectionKey.interestOps();
/**
* 1:ServerSocketChannel 初始化時 readInterestOp設定的是OP_ACCEPT事件
* 2:SocketChannel 初始化時 readInterestOp設定的是OP_READ事件
* */
if ((interestOps & readInterestOp) == 0) {
//註冊監聽OP_ACCEPT或者OP_READ事件
selectionKey.interestOps(interestOps | readInterestOp);
}
}
}
另一個就是這裡的readyOps
,用於記錄在Channel
感興趣的IO事件
中具體哪些IO事件
就緒了。
Netty中將各種事件的集合用一個int型
變數來儲存。
-
用
&
操作判斷,某個事件是否在事件集合中:(readyOps & SelectionKey.OP_CONNECT) != 0
,這裡就是判斷Channel是否對Connect事件感興趣。 -
用
|
操作向事件集合中新增事件:interestOps | readInterestOp
-
從事件集合中刪除某個事件,是通過先將要刪除事件取反
~
,然後在和事件集合做&
操作:ops &= ~SelectionKey.OP_CONNECT
Netty這種對空間的極致利用思想,很值得我們平時在日常開發中學習~~
現在我們已經知道哪些Channel
現在處於IO就緒狀態
,並且知道了具體哪些型別的IO事件
已經就緒。
下面就該針對Channel
上的不同IO就緒事件
做出相應的處理了。
3.1.2.1 處理Connect事件
Netty客戶端向服務端發起連線,並向客戶端的Reactor
註冊Connect事件
,當連線建立成功後,客戶端的NioSocketChannel
就會產生Connect就緒事件
,通過前面內容我們講的Reactor的執行框架
,最終流程會走到這裡。
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
//觸發channelActive事件
unsafe.finishConnect();
}
如果IO就緒
的事件是Connect事件
,那麼就呼叫對應客戶端NioSocketChannel
中的Unsafe操作類
中的finishConnect方法
處理Connect事件
。這時會在Netty客戶端NioSocketChannel
中的pipeline
中傳播ChannelActive事件
。
最後需要將OP_CONNECT事件
從客戶端NioSocketChannel
所關心的事件集合interestOps
中刪除。否則Selector
會一直通知Connect事件就緒
。
3.1.2.2 處理Write事件
關於Reactor執行緒
處理Netty中的Write事件
的流程,筆者後續會專門用一篇文章來為大家介紹。本文我們重點關注Reactor執行緒
的整體執行框架。
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
這裡大家只需要記住,OP_WRITE事件的註冊是由使用者來完成的,當Socket傳送緩衝區已滿無法繼續寫入資料時,使用者會向Reactor註冊OP_WRITE事件,等到Socket傳送緩衝區變得可寫時,Reactor會收到OP_WRITE事件活躍通知,隨後在這裡呼叫客戶端NioSocketChannel
中的forceFlush方法
將剩餘資料傳送出去。
3.1.2.3 處理Read事件或者Accept事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}
這裡可以看出Netty中處理Read事件
和Accept事件
都是由對應Channel
中的Unsafe操作類
中的read方法
處理。
服務端NioServerSocketChannel
中的Read方法
處理的是Accept事件
,客戶端NioSocketChannel
中的Read方法
處理的是Read事件
。
這裡大家只需記住各個
IO事件
在對應Channel
中的處理入口,後續文章我們會詳細分析這些入口函式。
3.1.3 從Selector中移除失效的SelectionKey
//用於及時從selectedKeys中清除失效的selectKey 比如 socketChannel從selector上被使用者移除
private boolean needsToSelectAgain;
//目的是再次進入for迴圈 移除失效的selectKey(socketChannel可能被使用者從selector上移除)
if (needsToSelectAgain) {
selectAgain();
selectedKeys = selector.selectedKeys();
// Create the iterator again to avoid ConcurrentModificationException
if (selectedKeys.isEmpty()) {
break;
} else {
i = selectedKeys.iterator();
}
}
在前邊介紹Reactor執行框架
的時候,我們看到在每次Reactor執行緒
輪詢結束,準備處理IO就緒事件
以及非同步任務
的時候,都會將needsToSelectAgain
設定為false
。
那麼這個needsToSelectAgain
究竟是幹嘛的?以及為什麼我們需要去“Select Again”
呢?
首先我們來看下在什麼情況下會將needsToSelectAgain
這個變數設定為true
,通過這個設定的過程,我們是否能夠從中找到一些線索?
我們知道Channel
可以將自己註冊到Selector
上,那麼當然也可以將自己從Selector
上取消移除。
在上篇文章中我們也花了大量的篇幅講解了這個註冊的過程,現在我們來看下Channel
的取消註冊。
public abstract class AbstractNioChannel extends AbstractChannel {
//channel註冊到Selector後獲得的SelectKey
volatile SelectionKey selectionKey;
@Override
protected void doDeregister() throws Exception {
eventLoop().cancel(selectionKey());
}
protected SelectionKey selectionKey() {
assert selectionKey != null;
return selectionKey;
}
}
Channel
取消註冊的過程很簡單,直接呼叫NioChannel
的doDeregister
方法,Channel
繫結的Reactor
會將其從Selector
中取消並停止監聽Channel
上的IO事件
。
public final class NioEventLoop extends SingleThreadEventLoop {
//記錄Selector上移除socketChannel的個數 達到256個 則需要將無效的selectKey從SelectedKeys集合中清除掉
private int cancelledKeys;
private static final int CLEANUP_INTERVAL = 256;
/**
* 將socketChannel從selector中移除 取消監聽IO事件
* */
void cancel(SelectionKey key) {
key.cancel();
cancelledKeys ++;
// 當從selector中移除的socketChannel數量達到256個,設定needsToSelectAgain為true
// 在io.netty.channel.nio.NioEventLoop.processSelectedKeysPlain 中重新做一次輪詢,將失效的selectKey移除,
// 以保證selectKeySet的有效性
if (cancelledKeys >= CLEANUP_INTERVAL) {
cancelledKeys = 0;
needsToSelectAgain = true;
}
}
}
- 呼叫
JDK NIO SelectionKey
的APIcancel方法
,將Channel
從Selector
中取消掉。SelectionKey#cancel方法
呼叫完畢後,此時呼叫SelectionKey#isValid
將會返回false
。SelectionKey#cancel方法
呼叫後,Selector
會將要取消的這個SelectionKey
加入到Selector
中的cancelledKeys集合
中。
public abstract class AbstractSelector extends Selector {
private final Set<SelectionKey> cancelledKeys = new HashSet<SelectionKey>();
void cancel(SelectionKey k) {
synchronized (cancelledKeys) {
cancelledKeys.add(k);
}
}
}
-
當
Channel
對應的SelectionKey
取消完畢後,Channel
取消計數器cancelledKeys
會加1,當cancelledKeys = 256
時,將needsToSelectAgain
設定為true
。 -
隨後在
Selector
的下一次
輪詢過程中,會將cancelledKeys集合
中的SelectionKey
從Selector
中所有的KeySet
中移除。這裡的KeySet
包括Selector
用於存放就緒SelectionKey
的selectedKeys集合
,以及用於存放所有註冊的Channel
對應的SelectionKey
的keys集合
。
public abstract class SelectorImpl extends AbstractSelector {
protected Set<SelectionKey> selectedKeys = new HashSet();
protected HashSet<SelectionKey> keys = new HashSet();
.....................省略...............
}
我們看到Reactor執行緒
中對needsToSelectAgain
的判斷是在processSelectedKeysPlain方法
處理IO就緒
的SelectionKey
的迴圈體中進行判斷的。
之所以這裡特別提到needsToSelectAgain
判斷的位置,是要讓大家注意到此時Reactor
正在處理本次
輪詢的IO就緒事件
。
而前邊也說了,當呼叫SelectionKey#cancel方法
後,需要等到下次輪詢
的過程中Selector
才會將這些取消的SelectionKey
從Selector
中的所有KeySet集合
中移除,當然這裡也包括就緒集合selectedKeys
。
當在本次
輪詢期間,假如大量的Channel
從Selector
中取消,Selector中的就緒集合selectedKeys
中依然會儲存這些Channel
對應SelectionKey
直到下次輪詢
。那麼當然會影響本次輪詢結果selectedKeys
的有效性。
所以為了保證Selector
中所有KeySet
的有效性,需要在Channel
取消個數達到256
時,觸發一次selectNow
,目的是清除無效的SelectionKey
。
private void selectAgain() {
needsToSelectAgain = false;
try {
selector.selectNow();
} catch (Throwable t) {
logger.warn("Failed to update SelectionKeys.", t);
}
}
到這裡,我們就對JDK 原生 Selector
的處理方式processSelectedKeysPlain方法
就介紹完了,其實 對IO就緒事件
的處理邏輯都是一樣的,在我們理解了processSelectedKeysPlain方法
後,processSelectedKeysOptimized方法
對IO就緒事件
的處理,我們理解起來就非常輕鬆了。
3.2 processSelectedKeysOptimized
Netty預設會採用優化過的Selector
對IO就緒事件
的處理。但是處理邏輯是大同小異的。下面我們主要介紹一下這兩個方法的不同之處。
private void processSelectedKeysOptimized() {
// 在openSelector的時候將JDK中selector實現類中得selectedKeys和publicSelectKeys欄位型別
// 由原來的HashSet型別替換為 Netty優化後的陣列實現的SelectedSelectionKeySet型別
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
// 對應迭代器中得remove selector不會自己清除selectedKey
selectedKeys.keys[i] = null;
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
-
JDK NIO 原生 Selector
存放IO就緒的SelectionKey
的集合為HashSet型別
的selectedKeys
。而Netty為了優化對selectedKeys 集合
的遍歷效率
採用了自己實現的SelectedSelectionKeySet型別
,從而用對陣列
的遍歷代替用HashSet
的迭代器遍歷。 -
Selector
會在每次輪詢到IO就緒事件
時,將IO就緒的Channel
對應的SelectionKey
插入到selectedKeys集合
,但是Selector
只管向selectedKeys集合
放入IO就緒的SelectionKey
,當SelectionKey
被處理完畢後,Selector
是不會自己主動將其從selectedKeys集合
中移除的,典型的管殺不管埋
。所以需要Netty自己在遍歷到IO就緒的 SelectionKey
後,將其刪除。- 在
processSelectedKeysPlain
中是直接將其從迭代器中刪除。 - 在
processSelectedKeysOptimized
中將其在陣列中對應的位置置為Null
,方便垃圾回收。
- 在
-
在最後清除無效的
SelectionKey
時,在processSelectedKeysPlain
中由於採用的是JDK NIO 原生的Selector
,所以只需要執行SelectAgain
就可以,Selector
會自動清除無效Key。
但是在processSelectedKeysOptimized
中由於是Netty自己實現的優化型別,所以需要Netty自己將SelectedSelectionKeySet
陣列中的SelectionKey
全部清除,最後在執行SelectAgain
。
好了,到這裡,我們就將Reactor執行緒
如何處理IO就緒事件
的整個過程講述完了,下面我們就該到了介紹Reactor執行緒
如何處理Netty框架中的非同步任務了。
4. Reactor執行緒處理非同步任務
Netty關於處理非同步任務
的方法有兩個:
-
一個是無超時時間限制的
runAllTasks()方法
。當ioRatio
設定為100
時,Reactor執行緒
會先一股腦的處理IO就緒事件
,然後在一股腦的執行非同步任務
,並沒有時間的限制。 -
另一個是有超時時間限制的
runAllTasks(long timeoutNanos)方法
。當ioRatio != 100
時,Reactor執行緒
執行非同步任務
會有時間限制,優先一股腦的處理完IO就緒事件
統計出執行IO任務
耗時ioTime
。根據公式ioTime * (100 - ioRatio) / ioRatio)
計算出Reactor執行緒
執行非同步任務
的超時時間。在超時時間限定範圍內,執行有限的非同步任務
。
下面我們來分別看下這兩個執行非同步任務
的方法處理邏輯:
4.1 runAllTasks()
protected boolean runAllTasks() {
assert inEventLoop();
boolean fetchedAll;
boolean ranAtLeastOne = false;
do {
//將到達執行時間的定時任務轉存到普通任務佇列taskQueue中,統一由Reactor執行緒從taskQueue中取出執行
fetchedAll = fetchFromScheduledTaskQueue();
if (runAllTasksFrom(taskQueue)) {
ranAtLeastOne = true;
}
} while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
if (ranAtLeastOne) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
//執行尾部佇列任務
afterRunningAllTasks();
return ranAtLeastOne;
}
Reactor執行緒
執行非同步任務
的核心邏輯就是:
-
先將到期的
定時任務
一股腦的從定時任務佇列scheduledTaskQueue
中取出並轉存到普通任務佇列taskQueue
中。 -
由
Reactor執行緒
統一從普通任務佇列taskQueue
中取出任務執行。 -
在
Reactor執行緒
執行完定時任務
和普通任務
後,開始執行儲存於尾部任務佇列tailTasks
中的尾部任務
。
下面我們來分別看下上述幾個核心步驟的實現:
4.1.1 fetchFromScheduledTaskQueue
/**
* 從定時任務佇列中取出達到deadline執行時間的定時任務
* 將定時任務 轉存到 普通任務佇列taskQueue中,統一由Reactor執行緒從taskQueue中取出執行
*
* */
private boolean fetchFromScheduledTaskQueue() {
if (scheduledTaskQueue == null || scheduledTaskQueue.isEmpty()) {
return true;
}
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
for (;;) {
//從定時任務佇列中取出到達執行deadline的定時任務 deadline <= nanoTime
Runnable scheduledTask = pollScheduledTask(nanoTime);
if (scheduledTask == null) {
return true;
}
if (!taskQueue.offer(scheduledTask)) {
// taskQueue沒有空間容納 則在將定時任務重新塞進定時任務佇列中等待下次執行
scheduledTaskQueue.add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
}
}
- 獲取當前要執行
非同步任務
的時間點nanoTime
final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFuture<V>, PriorityQueueNode {
private static final long START_TIME = System.nanoTime();
static long nanoTime() {
return System.nanoTime() - START_TIME;
}
}
- 從定時任務佇列中找出
deadline <= nanoTime
的非同步任務。也就是說找出所有到期的定時任務。
protected final Runnable pollScheduledTask(long nanoTime) {
assert inEventLoop();
//從定時佇列中取出要執行的定時任務 deadline <= nanoTime
ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
if (scheduledTask == null || scheduledTask.deadlineNanos() - nanoTime > 0) {
return null;
}
//符合取出條件 則取出
scheduledTaskQueue.remove();
scheduledTask.setConsumed();
return scheduledTask;
}
- 將
到期的定時任務
插入到普通任務佇列taskQueue
中,如果taskQueue
已經沒有空間容納新的任務,則將定時任務
重新塞進定時任務佇列
中等待下次拉取。
if (!taskQueue.offer(scheduledTask)) {
scheduledTaskQueue.add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
fetchFromScheduledTaskQueue方法
的返回值為true
時表示到期的定時任務已經全部拉取出來並轉存到普通任務佇列中。
返回值為false
時表示到期的定時任務只拉取出來一部分,因為這時普通任務佇列已經滿了,當執行完普通任務時,還需要在進行一次拉取。
當到期的定時任務
從定時任務佇列中拉取完畢或者當普通任務佇列已滿時,這時就會停止拉取,開始執行普通任務佇列中的非同步任務
。
4.1.2 runAllTasksFrom
protected final boolean runAllTasksFrom(Queue<Runnable> taskQueue) {
Runnable task = pollTaskFrom(taskQueue);
if (task == null) {
return false;
}
for (;;) {
safeExecute(task);
task = pollTaskFrom(taskQueue);
if (task == null) {
return true;
}
}
}
-
首先
runAllTasksFrom 方法
的返回值表示是否執行了至少一個非同步任務。後面會賦值給ranAtLeastOne變數
,這個返回值我們後續會用到。 -
從普通任務佇列中拉取
非同步任務
。
protected static Runnable pollTaskFrom(Queue<Runnable> taskQueue) {
for (;;) {
Runnable task = taskQueue.poll();
if (task != WAKEUP_TASK) {
return task;
}
}
}
Reactor執行緒
執行非同步任務
。
protected static void safeExecute(Runnable task) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception. Task: {}", task, t);
}
}
4.1.3 afterRunningAllTasks
if (ranAtLeastOne) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
//執行尾部佇列任務
afterRunningAllTasks();
return ranAtLeastOne;
如果Reactor執行緒
執行了至少一個非同步任務
,那麼設定lastExecutionTime
,並將ranAtLeastOne標識
返回。這裡的ranAtLeastOne標識
就是runAllTasksFrom方法
的返回值。
最後執行收尾任務,也就是執行尾部任務佇列中的尾部任務。
@Override
protected void afterRunningAllTasks() {
runAllTasksFrom(tailTasks);
}
4.2 runAllTasks(long timeoutNanos)
這裡在處理非同步任務
的核心邏輯還是和之前一樣的,只不過就是多了對超時時間
的控制。
protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
//普通佇列中沒有任務時 執行隊尾佇列的任務
afterRunningAllTasks();
return false;
}
//非同步任務執行超時deadline
final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
safeExecute(task);
runTasks ++;
//每執行64個非同步任務 檢查一下 是否達到 執行deadline
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
//到達非同步任務執行超時deadline,停止執行非同步任務
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
-
首先還是通過
fetchFromScheduledTaskQueue 方法
從Reactor
中的定時任務佇列中拉取到期的定時任務
,轉存到普通任務佇列中。當普通任務佇列已滿或者到期定時任務
全部拉取完畢時,停止拉取。 -
將
ScheduledFutureTask.nanoTime() + timeoutNanos
作為Reactor執行緒
執行非同步任務的超時時間點deadline
。 -
由於系統呼叫
System.nanoTime()
需要一定的系統開銷,所以每執行完64
個非同步任務
的時候才會去檢查一下執行時間
是否到達了deadline
。如果到達了執行截止時間deadline
則退出停止執行非同步任務
。如果沒有到達deadline
則繼續從普通任務佇列中取出任務迴圈執行下去。
從這個細節又可以看出Netty對效能的考量還是相當講究的
流程走到這裡,我們就對Reactor
的整個執行框架以及如何輪詢IO就緒事件
,如何處理IO就緒事件
,如何執行非同步任務
的具體實現邏輯就剖析完了。
下面還有一個小小的尾巴,就是Netty是如何解決文章開頭提到的JDK NIO Epoll 的空輪詢BUG
的,讓我們一起來看下吧~~~
5. 解決JDK Epoll空輪詢BUG
前邊提到,由於JDK NIO Epoll的空輪詢BUG
存在,這樣會導致Reactor執行緒
在沒有任何事情可做的情況下被意外喚醒,導致CPU空轉。
其實Netty也沒有從根本上解決這個JDK BUG
,而是選擇巧妙的繞過這個BUG
。
下面我們來看下Netty是如何做到的。
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;
} else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
//既沒有IO就緒事件,也沒有非同步任務,Reactor執行緒從Selector上被異常喚醒 觸發JDK Epoll空輪訓BUG
//重新構建Selector,selectCnt歸零
selectCnt = 0;
}
在Reactor執行緒
處理完IO就緒事件
和非同步任務
後,會檢查這次Reactor執行緒
被喚醒有沒有執行過非同步任務和有沒有IO就緒的Channel
。
-
boolean ranTasks
這時候就派上了用場,這個ranTasks
正是前邊我們在講runAllTasks方法
時提到的返回值。用來表示是否執行過至少一次非同步任務
。 -
int strategy
正是JDK NIO Selector
的select方法
的返回值,用來表示IO就緒
的Channel個數
。
如果ranTasks = false 並且 strategy = 0
這代表Reactor執行緒
本次既沒有非同步任務
執行也沒有IO就緒
的Channel
需要處理卻被意外的喚醒。等於是空轉了一圈啥也沒幹。
這種情況下Netty就會認為可能已經觸發了JDK NIO Epoll的空輪詢BUG
int SELECTOR_AUTO_REBUILD_THRESHOLD = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
private boolean unexpectedSelectorWakeup(int selectCnt) {
..................省略...............
/**
* 走到這裡的條件是 既沒有IO就緒事件,也沒有非同步任務,Reactor執行緒從Selector上被異常喚醒
* 這種情況可能是已經觸發了JDK Epoll的空輪詢BUG,如果這種情況持續512次 則認為可能已經觸發BUG,於是重建Selector
*
* */
if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// The selector returned prematurely many times in a row.
// Rebuild the selector to work around the problem.
logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.",
selectCnt, selector);
rebuildSelector();
return true;
}
return false;
}
- 如果
Reactor
這種意外喚醒的次數selectCnt
超過了配置的次數SELECTOR_AUTO_REBUILD_THRESHOLD
,那麼Netty就會認定這種情況可能已經觸發了JDK NIO Epoll空輪詢BUG
,則重建Selector
(將之前註冊的所有Channel重新註冊到新的Selector上並關閉舊的Selector
),selectCnt計數
歸0
。
SELECTOR_AUTO_REBUILD_THRESHOLD
預設為512
,可以通過系統變數-D io.netty.selectorAutoRebuildThreshold
指定自定義數值。
- 如果
selectCnt
小於SELECTOR_AUTO_REBUILD_THRESHOLD
,則返回不做任何處理,selectCnt
繼續計數。
Netty就這樣通過計數Reactor
被意外喚醒的次數,如果計數selectCnt
達到了512次
,則通過重建Selector
巧妙的繞開了JDK NIO Epoll空輪詢BUG
。
我們在日常開發中也可以借鑑Netty這種處理問題的思路,比如在專案開發中,當我們發現我們無法保證徹底的解決一個問題時,或者為了解決這個問題導致我們的投入產出比不高時,我們就該考慮是不是應該換一種思路去繞過這個問題,從而達到同樣的效果。*解決問題的最高境界就是不解決它,巧妙的繞過去~~~~~!!*
總結
本文花了大量的篇幅介紹了Reactor
整體的執行框架,並深入介紹了Reactor
核心的工作模組的具體實現邏輯。
通過本文的介紹我們知道了Reactor
如何輪詢註冊在其上的所有Channel上感興趣的IO事件,以及Reactor如何去處理IO就緒的事件,如何執行Netty框架中提交的非同步任務和定時任務。
最後介紹了Netty如何巧妙的繞過JDK NIO Epoll空輪詢的BUG,達到解決問題的目的。
提煉了新的解決問題的思路:解決問題的最高境界就是不解決它,巧妙的繞過去~~~~~!!
好了,本文的內容就到這裡了,我們下篇文章見~~~~~
歡迎關注公眾號:bin的技術小屋