Netty原始碼研究筆記(4)——EventLoop系列

邁吉發表於2022-05-23

1. Netty原始碼研究筆記(4)——EventLoop系列

EventLoop,即事件驅動,它是Netty的I/O模型的抽象,負責處理I/O事件、任務。

不同的EventLoop代表著不同的I/O模型,最重要、最主要的是NioEventLoop,表示多路複用的I/O模型,對應jdk的NIO。

NioEventLoop是單執行緒的,它通過將I/O事件的等待時間用於執行其他任務,從而提高了執行緒的利用率,提高了單執行緒的吞吐量。

然而使用者並不能直接使用EventLoop,應該使用EventLoopGroup,它代表了一組EventLoop。

1.1. 繼承關係

EventLoopGroup、EventLoop繼承自EventExecutorGroup、EventExecutor。

EventExecutor、EventExecutorGroup中定義了任務執行的相關方法,它們繼承於jdk的Executor系列,對其進行了增強,因此Netty同樣擴充套件了jdk的Future。

EventLoop、EventLoopGroup新增了註冊Channel的功能。

1.1.1. EventExecutorGroup

注意EventExecutorGroup繼承自ScheduledEventExecutorService,因此它有著提交延時任務的功能。

並且它繼承了Iterable介面,因此他可以輪詢自己管理的EventExecutor。

EventExecutorGroup管理一組EventExecutor,它在執行提交的task的時候,是從自己管理的EventExecutor中選出一個,轉交給它來執行。

EventExecutorGroup提供的功能有:

  • next:選擇一個EventExecutor將其返回。

  • iterator:返回遍歷自己管理的EventExecutor集合的迭代器。

  • shutDownGracefully:將自己管理的所有EventExecutor給優雅的關閉,可以指定timeout,也可以不指定timeout。

  • isShuttingDown:自己管理的所有EventExecutor是否正在shutDownGracefully,或已經shutdown了。

  • terminationFuture:返回自己的terminationFuture,它指示EventExecutorGroup的關閉動作的執行情況。

  • 重寫自ExecutorService、ScheduledExecutorService的方法:submit、schedule、scheduleAtFixedRate、scheduleWithFixedDelay。然而重寫的時候並沒有取改變方法的入參、返回引數,這些方法簽名資訊,為什麼要重寫呢?那是因為對於EventExeucotGroup來說,它的這些任務執行方法和ExecutorService、ScheduledExecutorService的語義有差別,它額外新增了這個任務的執行是由EventExecutorGroup去執行,最終是交給其管理的EventExecutor來執行這個語義,因此和jdk的語義有所區分。

Netty原始碼研究筆記(4)——EventLoop系列

1.1.2. EventExecutor

EventExecutor的實際實現類有:DefaultEventExecutor、GlobalEventExecutor、NoStickyOrderedEventExecutor、UnorderedThreadPoolEventExecutor、ImmediateEventExecutor。

其中後兩者不是OrderedEventExecutor,前三者都是OrderedEventExecutor。

所謂的OrderedEventExecutor是指,同一執行緒提交的任務,保證它們的執行順序為提交順序。

EventExecutor在EventExecutorGroup的基礎上還增加了:

  1. 建立Future、Promise的功能
  2. 判斷給定執行緒是否在EventLoop中的功能
  3. 獲取自己所在的EventExecutorGroup的功能
  4. 重寫了EventExecutorGroup的next方法,對於前者,它返回其管理的一個EventExecutor,對於EventExecutor來說該方法返回自身。
Netty原始碼研究筆記(4)——EventLoop系列

1.1.3. EventLoopGroup

EventLoopGroup管理一組EventLoop,它繼承自EventExecutorGroup,它在EventExecutorGroup的基礎上提供了註冊Netty Channel的功能。

Netty原始碼研究筆記(4)——EventLoop系列

1.1.4. EventLoop

除了EmbeddedEventLoop不是OrderedEventExecutor,其他的都是OrderedEventExecutor(都繼承自SingleThreadEventLoop)

EventLoop介面內並沒有新增新的功能,它只是重寫parent方法,將返回型別改為EventLoopGroup。

Netty原始碼研究筆記(4)——EventLoop系列

1.2. AbstractEventExecutorGroup

由於EventExecutorGroup在執行提交的任務的時候是轉交給自己管理的EventExecutor,所以AbstractEventExecutorGroup除此之外沒有別的過程細節了,並且AbstractEventExecutorGroup的實現上也只做了這一件事,對於獲取管理的EventExecutor、優雅關閉,這些非執行任務型別的方法,它沒有實現。

1.3. MultiThreadEventExecutorGroup

MultiThreadEventExecutorGroup繼承於AbstractEventExecutorGroup。

AbstractEventExecutorGroup負責的是任務執行的相關功能。

MultiThreadEventExecutorGroup負責的剩下的其他的事項:

  • event executor的管理
  • shutdown、terminate相關

注意,MultiThreadEventExecutorGroup不負責建立它所管理的EventExecutor,這個child的建立工作(newChild方法)留給了自己的子類取實現。

然後它還把從自己管理的EventExecutor挑出來一個交給請求方這個功能又剝離開來,交給EventExecutorChooser來實現。

MultiThreadEentExecutorGroup,它在建立自己管理的EventExecutor的時候,為這每個待建立的EventExecuto都提供了一個單執行緒的Executor(ThreadPerTaskExecutor),因此被稱為是multi thread event executor group。

MultiThreadEventExecutorGroup是一個抽象類,在構造它的時候需要提供執行緒的數量,執行緒的數量即為它管理的event executor數量。

然後其建構函式的多餘的入參,其實是轉交給newChild了,用來建立event executor。

1.4. DefaultEventExecutorGroup

這個沒什麼好說,它實現了MultiThreadEventExecutorGroup的newChild為建立DefaultEventExecutor,並且對於預設構造引數的情況下,設定預設的任務拒絕策略為拒絕(這些拒絕策略在RejectedExecutionHandlers中,目前一共就兩種,還有一種是backoff),設定預設的pending task數量為最大的Integer。

1.5. AbstractEventExecutor

AbstractEventExecutor幾乎是所有EventExecutor的共同祖先,除了UnOrderedThreadPoolEventExecutor。

UnOrderedThreadPoolEventExecutor,是基於jdk的ScheduledThreadPoolExecutor來實現的EventExecutor的功能,其他的EventExecutor都基於是netty自己的實現。

AbstractEventExecutor實現了EventExecutor介面中新加的功能,見上面EventExecutor:

  1. 建立Future、Promise的功能

  2. 判斷給定執行緒是否在EventLoop中的功能

  3. 獲取自己所在的EventExecutorGroup的功能

AbstractEventExeutor繼承於jdk的AbstractExecutorService,因此它還有些配合AbstractExecutorService的方法。

比如:將newTaskFor方法需要返回的RunnableFuture(這個介面繼承於Runnable、Future,future的結果為run結束後得到的結果)返回為netty自己實現的PromiseTask。

然後schedule相關的方法,AbstractEventExecutor暫不支援,直接拋異常。

1.6. AbstractScheduledEventExecutor

AbstractScheduledEventExecutor繼承於EventExecutor,它實現了跟延時任務的相關方法,實現上使用ScheduledFutureTask來實現,注意,在時間方面上使用的是nano time,這個跟current time millis不一樣,前者是跟當前JVM啟動的時間起開始算,而後者是從GMT1970年1月1日0時到現在當前的毫秒數。

AbstractScheduledEventExecutor內部使用了netty自己的特殊的優先順序佇列來裝這些ScheduledFutureTask。

不管延時任務、還是週期任務,它們只是構造ScheduledFutureTask時的入參不同而已。再實現延時/週期執行的功能時,AbstractScheduledEventExecutor只是提供了一個骨架程式碼,關鍵的功能實現還是要看AbstractScheduledEventExecutor的子類對其定義的
hook函式的實現,ScheduledFutureTask在執行時候的內部實現,以及兩者間的配合。

1.6.1. schedule方法

其他所有public的schedule方法都是呼叫了這個私有的schedule方法,它們負責建立ScheduledFutureTask。

其中execute、lazyExecute、beforeScheduledTaskSubmitted、afterScheduledTaskSubmitted這幾個方法都待子類來實現。

方法解釋:

如果schedule方法在EventLoop內部被呼叫,那麼簡單的將任務放到週期、延時任務佇列中,因為被呼叫時EventLoop肯定不是處於IO的等待阻塞狀態,所以這樣做安全。(因為EventLoop在IO阻塞前會看deadline最近的延時任務的deadline,從而計算出阻塞的超時時間。)

如果是在EventLoop之外被呼叫,這裡就NioEventLoop進行說明:

如果EventLoop正在select阻塞著,並且下一次被喚醒在這個task的deadline之後,那麼就將這個任務新增到SingleThreadEventExecutor的普通任務佇列中,並喚醒EventLoop。

反之如果EventLoop當前沒有阻塞,或阻塞但下一次被喚醒時間在這個task的deadline之前,那麼就將這個任務新增到SingleThreadEventExecutor的普通任務佇列中,新增完後再次判斷一下是否到deadline了,如果到了就嘗試喚醒EventLoop(而不管EventLoop當前是否阻塞)。

Netty原始碼研究筆記(4)——EventLoop系列

1.7. SingleThreadEventExecutor

SingleThreadEventExecutor是EventLoop體系的一個核心類(另外一個核心類是NioEventLoop),它負責實現整個的EventExecutor生命週期的管理:

比如:execute、shutdownGracefully等方法。

SingleThreadEventExecutor需要通過一個Executor來建立,然後啟動後它會獨佔這個Executor中的一個執行緒,它只負責管理生命週期,至於EventLoop具體怎麼loop,它不負責,這個它抽象出來個run方法,留給自己子類去實現,比如對於DefaultEventExecutor,它的實現是個死迴圈,然後不斷從佇列中獲取任務執行,並更新上一次的執行時間,對於NioEventLoop,它的loop實現中,不僅要從佇列中取出任務執行,還需要處理IO事件。

SingleThreadEventExecutor的state依次為:not startedstartedshuttingdownshutdownterminated

Netty原始碼研究筆記(4)——EventLoop系列

1.7.1. execute方法

SingleThreadEventExecutor在執行任務時是通過將task新增到自己內部的普通任務佇列中(區別於AbstractScheduledEventExecutor中的週期任務佇列:PriorityQueue)。

SingleThreadEventExecutor被建立時還沒有開啟事件迴圈執行緒,當提交第一個任務後才開啟事件迴圈執行緒。

如果提交不是LazyRunnable,那麼將任務入隊之後還要喚醒EventLoop執行緒。

如果是從外部提交任務,提交完成後發現狀態已經是ST_SHUTDOWN及以後了,那麼就嘗試從任務佇列中移除當前task,如果移除成功就reject,如果移除失敗,那麼說明這個任務得到了執行。

addTasksWakesUp,這個欄位是子類在繼承SingleThreadEventExecutor時,要告知給它的一個指示性欄位,表示在子類的實現中,addTask方法會不會喚醒SingleThreadEventExecutor所在的執行緒。如果會喚醒的話,那麼SingleThreadEventExecutor就沒必要多餘再手動的呼叫wakeup函式了。

SingleThreadEventExecutor的wakeup函式的實現是向任務佇列新增一個WAKE_UP任務,對於需要處理IO的子類NioEventLoop來說,它還會呼叫selector的wakeup函式。

Netty原始碼研究筆記(4)——EventLoop系列 Netty原始碼研究筆記(4)——EventLoop系列

startThread:

startThead方法在execute時候被呼叫,用於啟動一次,它採用對SingleThreadEventExecutor的state欄位的CAS操作,來保證只會被真正startThread一次。

Netty原始碼研究筆記(4)——EventLoop系列

doStartThread:

真正表示SingleThreadEventExecutor執行時宣告週期的其實在doStartThread方法中。

doStartThread就是向SingleThreadEventExecutor持有的executor(預設為ThreadPerTaskExecutor)新增一個任務,這個任務本身是一個事件迴圈(死迴圈),它只有在shutdown時或者執行任務時遇到無法處理的異常時被打斷。

這個方法看起來很長,實際上大部分篇幅都在999行起的finally語句塊中跟shutdown相關的程式碼,程式碼執行到它時分兩種情況:

  • 被shutdown了(這種情況success被設定為true)

  • 子類實現的run方法在執行時出現了不能處理的異常。(這種情況success為false)

事件迴圈(run方法)結束後,首先設定狀態為shutting down(迴圈CAS直到成功為止),因為可能是因為事件迴圈拋異常進這兒來的。

然後檢查子類的事件迴圈的實現中,結束時,是否呼叫了confirmShutdown方法,如果沒有呼叫,那麼日誌:子類的實現有bug。

然後執行ST_SHUTTING_DOWN到ST_SHUTDOWN狀態切換前的過渡工作(在迴圈供不斷的執行cofirmShutdown方法,判斷是否結束這個過渡期),這個過渡工作跟shutdownGracefully方法的quietPeriod、timeout入參有關,這個過渡工作就是說:在這個狀態切換的前,event executor會等待quietPeriod這麼長的時間,如果在這段時間外界又提交了新的任務,那麼執行這些任務,並且又重新等待quietPeriod這麼長的時間,直到連續的靜默期超過了quietPeriod,或者在ST_SHUTTING_DOWN狀態的總停留時間達到了timeout,那麼這個過渡期才結束。

過渡期結束後,就將狀態設定為ST_SHUTDOWN。

然後再confirmShutdown一次,因為設定狀態為ST_SHUTDOWN的時候是迴圈CAS操作,在狀態設定成功前,仍可能有任務新增進來了,因此我們需要執行這部分任務。

然後呼叫cleanup方法,它是一個protect方法,給子類實現,對於NioEventLoop來說,它關閉selector。

然後移除本執行緒所有的FastThreadLocal、丟棄掉立即任務佇列中剩餘的所有任務、terminationFuture設定為success、狀態設定為terninated。

Netty原始碼研究筆記(4)——EventLoop系列 Netty原始碼研究筆記(4)——EventLoop系列

confirmShutdown方法:

confirmShutdown在自定義的EventLoop結束的前(shutdown)需要被呼叫,返回true才能退出eventloop。

confirmShutdwon返回的結果表示,是否可以進入ST_SHUTDOWN了,confirmShutdown在狀態為ST_SHUTTING_DOWN的時候起作用,是這兩個狀態切換前的check。

gracefulShutdownQuietPeriod表示ST_SHUTTING_DOWN的靜默時間閾值,EveentExecutor的ST_SHUTTING_DOWN狀態的連續靜默時間(這段時間內外部沒有提交任務)超過了gracefulShutdownQuietPeriod,或者EventExecutor的ST_SHUTTING_DOWN狀態的總時間超過了gracefullyTimeout,EventExecutor才能確保自己能從shuttingdown切換為shutdown。

當EventExecutor狀態為shuttingdown時,在confirmShutdown時如果發現沒有任務或shutdown hook可以執行時,那麼就說明自己目前是quiet的,這時每隔100ms檢查一次788行跟1022行結合起來看。

779行對於gracefullyShutdownTimeout的判斷其實算是一種被動的判斷,就是說如果shuttingdown了,當外部執行緒提交task的速度超過了EventExecutor 在迴圈confirmShutown時執行任務的速度,那麼gracefullyTimeout不會被檢查到,沒起到作用,但只要提交task的速度有一點跟不上,也就是說出現了一絲Quiet的機會,shutdown超時就會通過779行對gracefullShutdownTimeout的檢查時被捕捉到。

注:不知道為什麼confirmShutdown方法裡面需要給queue新增WAKE_UP任務,感覺沒必要啊,因為這個方法都是在事件迴圈所線上程被呼叫,並且這個方法被呼叫的時候,該執行緒自身肯定沒有被block在這個queue上啊(這個queue在SingleThreadEventExecutor中預設是LinkedBlockingQueue,在NioEventLoop中是JCT的非阻塞的Mpsc queue)。

Netty原始碼研究筆記(4)——EventLoop系列 Netty原始碼研究筆記(4)——EventLoop系列

1.7.2. shutdownGracefully方法

由於shutdown方法已經被廢棄,所以我們不看,只看shutdownGracefully。

無入參的shutdownGracefully在AbstractEventExecutor中實現了,它使用quietPeriod為2s,timeout為15s,來呼叫有入參的shutdownGracefully方法。

shutdownGracefully內部使用迴圈+CAS操作來保證只有一個執行緒會將SingleThreadEventExecutor的狀態設定為shuttingdown。

出634行的for迴圈只有兩種情況:

  • 成功的改變了SingleThreadEventExecutor的狀態為shuttingdown,這時,如果之前event executor還沒有啟動,那麼還會將其啟動。需要wakeup。

  • 在635行狀態還不是shuttingdown,但是在640行狀態就成shuttingdown了,這時CAS操作的oldstate和newstate相同,CAS了個寂寞,不過在這種情況接下來就不需要wakeup了,因為說明已經有另外的執行緒在做shutdown的了。這種情況下後面的程式碼ensureThreadStarted時也不會再doStartThread,只是修改了gracefulShutdownQuietPeriod、gracefulShutdownTime這兩個欄位。這兩個欄位只在confirmShutdown時使用到。

Netty原始碼研究筆記(4)——EventLoop系列 Netty原始碼研究筆記(4)——EventLoop系列

1.8. MultiThreadEventLoopGroup

MultiThreadEventLoopGroup中定義了EventLoop的預設執行緒數量,它是CPU核心數的兩倍。

注意,這個執行緒的數量的設定是有講究的:

  • 計算密集性:CPU核心數 + 1。
  • IO密集性:CPU 核心數 *(1 + 平均等待時間/平均工作時間),一般設定為CPU核心數兩倍。
  • 混合性: CPU核心數 / 計算密集任務佔總任務的比例

如果我們構造EventLoopGroup的時候沒有提供執行緒數量,那麼實際使用這個預設數量。

它在註冊Channel的時候,是從自己管理的EventLoop中選一個出來,用它來註冊Channel。

1.9. NioEventLoopGroup

NioEventLoopGroup做了剩下的工作,它內部是一大堆各種引數的建構函式,這些引數配合newChild方法,從而能建立出自己管理的NioEventLoop。

1.10. SingleThreadEventLoop

SingleThreadEventLoop完成了註冊Channel的功能。它內部還定義了一個tailTasks這樣一個queue,它的作用是,我們可能想再每一次eventloop結束時,這個時間點來執行一些任務,比如說統計eventloop次數之類的,然後這些任務會放在這個queue中,新增這類任務的方法是:executeAfterEventLoopIteration。

除此之外,SingleThreadEventLoop還定義了SingleThreadEventExecutor的任務佇列的最大容量的預設值:io.netty.eventLoop.maxPendingTasks這個啟動引數設定它,預設是Integer.MAX_VALUE,最小為16。

1.11. NioEventLoop

NioEventLoop是EventLoop體系的另外一個核心類,上一個是SingleThreadEventExecutor。

它實現了事件迴圈內部的細節。通過將普通的任務的處理和IO的等待時間重疊,從而提高了單執行緒的利用率,提高了吞吐量。

由於SingleThreadEventExecutor負責的是任務的執行,所以NioEventLoop補充的是對IO處理。

NioEventLoop是基於JDK的NIO,因此它持有的欄位都是圍繞著IO的處理展開,都是圍繞jdk的nio展開。

下面講NioEventLoop的一些特性:

  • NioEventLoop使用的任務佇列是JCT的MPSC(多生產者單消費者非阻塞佇列)。

  • NioEventLoop還對jdk的selector進行了優化:

    這個優化預設是開啟的,可以將啟動引數io.netty.noKeySetOptimization設定為false來禁用。

    jdk的SelectorImpl內部在存放selection key的時候,用的是HashSet——即keys、selectedKeys、publicKeys、publicSelectedKeys這四個欄位,前兩個宣告為Set<SelectionKeys>型別,在構造器中實際建立為HashSet,後兩個是前兩個的unmodifiableSet、ungrowableSet,前兩者不暴露給使用者,暴露的是後兩者,這樣防止使用者對其錯誤修改了——keys()、selectedKeys()返回的就是後兩者。然後jdk的selector實現中,在register、select操作的時候,修改的是前兩者,這樣連帶著後兩者也改變了(原集合和unmodifiable集合之間共享資料,unmodifiable只是禁止了修改的操作,並不是copy資料後獨立出來),因此使用者可以及時的拿到更新。如果不用netty的話,使用者需用迭代器來遍歷selected key,並且在處理完之後要使用迭代器的remove方法來移除處理完的key。

    netty的優化是,將selectedKeys、publicSelectedKeys這兩個欄位通過反射,替換成了陣列的實現(SelectedSelectionKeySet),並且它們指向同一個例項,因為add操作很頻繁,而HashSet在面對碰撞的時候效率會降低。然後把這個SelectedSelectionKeySet和這個jdk的selector打包起來建立為SelectedSelectionKeySetSelector,netty用的就是這個打包後的selector,之前的jdk的selector就成了unwrapped selector了。

    這個SelectedSelectionKeySetSelector對於selector的實現都是委託給unwrapped selector,只是對於select、selectNow方法,它會將SelectedSelectionKeySet先reset一遍,為啥要reset一遍呢,因為jdk的selector的實現中,在進行select、selectNow操作的時候,因為用的是Set來存key所以只是簡單的add(Set會自動去重),然而,SelectedSelectionKeySet在add的時候只是將陣列的下標往後移動而已,不自帶去重功能,所以如果不reset的話,那麼同一個key就會存多次。並且這些select操作的時候,並不會自動的將上一次select到的,而這次沒有select到的key從selectedKeys欄位中移除,因此對於直接使用NIO的使用者來說,遍歷處理selected key時,處理完後要呼叫迭代器的remove方法,所以reset也有這樣的作用,相當於一次將所有都remove了,然後再次select時,留下來的都是這次select到的key。

我們不能直接使用NioEventLoop,我們應該使用NioEventLoopGroup,NioEventLoop構造引數挺多的,它被NioEventLoopGroup管理並構造。

1.11.1. run方法(事件迴圈)

run方法在SingleThreadEventExecutor的doStartThread中被呼叫到,它是一個迴圈體,是eventloop的本體,跳出它的迴圈之後就是shutdown的邏輯了(doStartThread中)。

思路是不存在立即任務的時候使用selector阻塞select,存在立即任務的時候時候則使用selector的非阻塞方法selectNow獲取selectionKey,然後將selectionKey的處理和task的執行放在一塊,利用ioRatio(io比例,也就是sectionKey處理時間比例)來劃分兩者的執行時間。將task的執行時間和selectionKey的等待時間重疊在一起。

每次迴圈首先計算strategy:當SingleThreadEventExecutor的taskQueue或SingleThreadEventLoop的tailTasks佇列不為空時(這兩個佇列中的存的是立即任務),就使用selector的selectNow(非阻塞),它返回的是當前SelectionKey的數量;當沒有立即任務的時候,返回SelectStrategy.SELECT。

也就是說,當有立即任務時,本次迴圈就不阻塞等待IO了,直接進入後面的SelectionKey和任務的處理邏輯。而當沒有立即任務時,本次迴圈就可以阻塞:獲取最近將要執行的延時任務的deadline記錄到nextWakeupNanos欄位中(該欄位可以幫助我們判斷EventLoop是否阻塞著,以及下次什麼時候會喚醒,它在AbstractScheduledEventExecutor schedule一個延時任務時用到),然後用這個deadline和當前時間的間隔作為本次阻塞時間(如果阻塞時間小於5ms,就不阻塞了),阻塞之前再次檢查一下是否有立即任務。阻塞結束後(被wakeup或到期),會將nextWakeupNanos設為-1,表示AWARE。

上面跟selector有關的操作是在一個try catch塊中進行的,如果出現了異常,那麼會rebuildSelector,並且將selectCnt(表示當前連續空select的次數,由於JDK 在linux 平臺上有臭名昭著的epoll bug)置為0。

當上面決定不阻塞,或者阻塞結束,就進入下面的處理邏輯:SelectionKey和週期任務、立即任務。

SelectionKey的處理和任務的執行的時間比例劃分由ioRatio來決定:

  • 如果ioRatio為100時,表示所有的執行時間優先分配給io處理,呼叫processSelectedKeys,selectionKey處理完後再執行當前所能執行的所有task。

  • ioRatio不為100時,判斷當前有沒有準備好的sectionKey:如果有的話就先處理所有準備好的selectionKey,然後根據它們的處理時間以及ioRatio,計算出task的執行時間(timeout)然後執行task;如果當前沒準備好的selectionKey,那麼就最多執行64個task(儘可能少地執行任務)。

task和SelectionKey process後,檢查是否需要rebuilde selector:如果處理階段沒有執行任務且沒有處理SelectionKey,那麼就有發生空輪詢bug的可能性:這時如果是因為執行緒中斷而提前喚醒,那麼就清空selectCnt(當前連續空輪詢次數);如果不是執行緒中斷,且當前selectCnt達到了SELECTOR_AUTO_REBUILD_THRESHOLD(通過io.netty.selectorAutoRebuildThreshold啟動引數配置,預設512,如果配的小於3,那麼就設定為0),那麼就判定此時發生了空輪詢,此時要rebuild selector,然後重置selectCnt。

rebuild selector的時候,不僅是建立新的selector,並且還要將註冊在舊selector上的channel重新註冊到新的selector上,並且將得到的selection key替換到對應的Netty Channel中(Netty channel是作為attachment,從而關聯上selection key的)。

每輪迴圈分上面幾個階段,它們在一個大的try語句塊中,這幾個階段中出現error後,run方法就結束並向外拋這個error,出現異常了就呼叫異常處理邏輯(睡眠1s),當每輪迴圈的這兩個階段結束後,會檢查EventLoop是否已經呼叫了shutdown:如果是就獲取到selector管理的所有Channel(netty channel,它作為SelectionKey的attachment),使用它們的unsafe物件來close它們,然後confirmShutdown之後退出run方法;如果當前沒有shutdown,那麼就繼續下一輪迴圈。

Netty原始碼研究筆記(4)——EventLoop系列 Netty原始碼研究筆記(4)——EventLoop系列 Netty原始碼研究筆記(4)——EventLoop系列

1.11.1.1. processSelectedKeys

selectedKeys即為上面的對selector優化的SelectedSelectionKeySet。如果它為null,表示禁用了selector優化,反之沒有。

因此進入了兩種不同的process邏輯:

  • 一種為optimized
  • 一種為plain
Netty原始碼研究筆記(4)——EventLoop系列

processSelectedKeysOptimized:

之前select操作拿到的key都放在selectedKeys這個欄位中,內部為陣列儲存,因此只需要依次按下標遍歷即可。

每遍歷一個key,就將其在陣列中的槽位置空,這樣對哪些已經close掉的channel能夠幫助它們GC。

每處理一個key的時候,都需要檢查是否要重新select(selectNow):當netty channel 向其註冊的eventloop進行deregister的時候,那麼會將selection key給取消,並且增加eventloop的cancel key計數——cancelledKeys,注意這個計數只是在processSelectedKeys期間有效,eventloop每迭代一次都會將置為0。當processSelectedKeys期間的cancelledKeys計數超過了閾值(硬編碼為256),那麼就需要重新selectNow。
當需要重新select的時候,會將selectedKeys給清空(所有槽位置為null,並且size變0,因為之前處理的已經給置null了,所以只需要從i+1下標位置開始置null)。

Netty原始碼研究筆記(4)——EventLoop系列

processSelectedKeysPlain:

processSelectedKeysPlain跟processSelectedKeysOptimized要做的事情差不多,只是在沒有優化selector的場景下,需要跟平時自己手動使用NIO一樣,採用迭代器來遍歷selected keys。

Netty原始碼研究筆記(4)——EventLoop系列

processSelectedKey:

processSelectedKey方法有兩個過載方法,我們忽略第二個入參為NioTask型別的processSelectedKey方法,只看第二入參型別為AbstractNioChannel型別的方法。

首先對選的key的有效性進行判斷,如果key無效了,要判斷對應的channel是否還註冊在本eventloop上,如果是,那麼才有許可權對其進行close,否則不能close。

AbstractChannel在deregister的時候,將deregister動作交給pipeline,pipeline交給tail ctx,最終在head ctx呼叫AbstractChannell的unsafe的deregister。

AbstractUnsafe的deregister過程並沒有將Channel的eventloop欄位置為空,只是把unsafe的registered欄位置為false。那麼在692行會不會出現競態?答案是不會,因為unsafe在執行deregister的時候把這個動作作為一個task交給eventloop執行(Unsafe的操作基本都放在IO執行緒中做),而eventloop是單執行緒,自然不會出現競態:因為deregister後,在下輪的select中就會把過期的SelectionKey給清除了,也就是說本輪處理SelectionKey,取消動作還沒執行(IO先於任務),而SelectionKey的取消動作執行後,下一輪開頭Select動作又把它清掃了,所以對於deregister而言不會走到678的if語句塊中。但是如果對於disconnect、close這些操作,如果說同一個eventloop中的channel在處理IO的時候去disconnect、close其他channel,那麼輪到該channel的IO處理時,就可能進入678的if塊中。

當SelectionKey的OP_CONNECT就緒後,就不再關注OP_CONNECT,並呼叫unsafe的finishConnect方法。

當SelectionKey的OP_WRITE就緒後,就呼叫unsafe的forceFlush方法

當SelectionKey沒有就緒的OP或SelectionKey的OP_ACCEPT或OP_READ就緒後,就呼叫unsafe的read方法

注:上面三句話特別重要。

Netty原始碼研究筆記(4)——EventLoop系列 Netty原始碼研究筆記(4)——EventLoop系列

相關文章