Netty 源碼解析 ——— NioEventLoop 詳解

本文是Netty文集中“Netty 源碼解析”系列的文章。主要對Netty的重要流程以及類進行源碼解析,以使得我們更好的去使用Netty。Netty是一個非常優秀的網絡框架,對其源碼解讀的過程也是不斷學習的過程。

NioEventLoop

通過前面的學習,我們對NioEventLoop做過如下幾點簡單的概述:
① NioEventLoop是一個基于JDK NIO的異步事件循環類,它負責處理一個Channel的所有事件在這個Channel的生命周期期間。
② NioEventLoop的整個生命周期只會依賴于一個單一的線程來完成。一個NioEventLoop可以分配給多個Channel,NioEventLoop通過JDK Selector來實現I/O多路復用,以對多個Channel進行管理。
③ 如果調用Channel操作的線程是EventLoop所關聯的線程,那么該操作會被立即執行。否則會將該操作封裝成任務放入EventLoop的任務隊列中。
④ 所有提交到NioEventLoop的任務都會先放入隊列中,然后在線程中以有序(FIFO)/連續的方式執行所有提交的任務。
⑤ NioEventLoop的事件循環主要完成了:a)已經注冊到Selector的Channel的監控,并在感興趣的事件可執行時對其進行處理;b)完成任務隊列(taskQueue)中的任務,以及對可執行的定時任務和周期性任務的處理(scheduledTaskQueue中的可執行的任務都會先放入taskQueue中后,再從taskQueue中依次取出執行)。

其中幾點已經在啟動流程的源碼分析中做了詳細的介紹。本文主要針對NioEventLoop事件循環的流程對NioEventLoop進行更深一步的學習。

JCTools

JCTools:適用于JVM的Java并發工具。該項目旨在提供一些當前JDK缺失的并發數據結構。
SPSC/MPSC/SPMC/MPMC 變種的并發隊列:

  • SPSC:用于單生產者單消費者模式(無等待,有限長度 和 無限長度)
  • MPSC:用于多生產者單消費者模式(無鎖的,有限長度 和 無限長度)
  • SPMC:用于單生產者多消費者模式(無鎖的,有限長度)
  • MPMC:用于多生產者多消費模式(無鎖的,有限長度)

JCTools提供的隊列是一個無鎖隊列,也就是隊列的底層通過無鎖的方式實現了線程安全的訪問。
MpscUnboundedArrayQueue是由JCTools提供的一個多生產者單個消費者的數組隊列。多個生產者同時并發的訪問隊列是線程安全的,但是同一時刻只允許一個消費者訪問隊列,這是需要程序控制的,因為MpscQueue的用途即為多個生成者可同時訪問隊列,但只有一個消費者會訪問隊列的情況。如果是其他情況你可以使用JCTools提供的其他隊列。

Q:為什么說MpscUnboundedArrayQueue的性能高于LinkedBlockingQueue了?
A:① MpscUnboundedArrayQueue底層通過無鎖的方式實現了多生產者同時訪問隊列的線程安全性,而LinkedBlockingQueue是一個多生產者多消費者的模式,它則是用過Lock鎖的方式來實現隊列的線程安全性。
② Netty的線程模型決定了taskQueue可以用多個生產者線程同時提交任務,但只會有EventLoop所在線程來消費taskQueue隊列中的任務。這樣JCTools提供的MpscQueue完全符合Netty線程模式的使用場景。而LinkedBlockingQueue會在生產者線程操作隊列時以及消費者線程操作隊列時都對隊列加鎖以保證線程安全性。雖然,在Netty的線程模型中程序會控制訪問taskQueue的始終都會是EventLoop所在線程,這時會使用偏向鎖來降低線程獲得鎖的代價。

偏向鎖:HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程再進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word(Mark Word是Java對象頭的內容,用于存儲對象的hashCode或鎖信息等)里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。

重要屬性

  • taskQueue
// 用于存儲任務的隊列,是一個MpscUnboundedArrayQueue實例。
private final Queue<Runnable> taskQueue;
  • tailTasks
// 是一個MpscUnboundedArrayQueue實例。用于存儲當前或下一次事件循環(eventloop)迭代結束后需要執行的任務。
private final Queue<Runnable> tailTasks;
  • scheduledTaskQueue
// 定時或周期任務隊列,是一個PriorityQueue實例。
Queue<ScheduledFutureTask<?>> scheduledTaskQueue;
  • SELECTOR_AUTO_REBUILD_THRESHOLD
private static final int MIN_PREMATURE_SELECTOR_RETURNS = 3;
private static final int SELECTOR_AUTO_REBUILD_THRESHOLD;
int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) {
    selectorAutoRebuildThreshold = 0;
}
SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;

SELECTOR_AUTO_REBUILD_THRESHOLD用于標識Selector空輪詢的閾值,當超過這個閾值的話則需要重構Selector。
如果有設置系統屬性”io.netty.selectorAutoRebuildThreshold”,并且該屬性值大于MIN_PREMATURE_SELECTOR_RETURNS(即,3),那么該屬性值就為閾值;如果該屬性值小于MIN_PREMATURE_SELECTOR_RETURNS(即,3),那么閾值為0。如果沒有設置系統屬性”io.netty.selectorAutoRebuildThreshold”,那么閾值為512,即,默認情況下閾值為512。

  • selectNowSupplier
private final IntSupplier selectNowSupplier = new IntSupplier() {
    @Override
    public int get() throws Exception {
        return selectNow();
    }
};

selectNow提供器,在事件循環里用于選擇策略(selectStrategy)中。

  • pendingTasksCallable
private final Callable<Integer> pendingTasksCallable = new Callable<Integer>() {
    @Override
    public Integer call() throws Exception {
        return NioEventLoop.super.pendingTasks();
    }
};
@Override
public int pendingTasks() {
    // As we use a MpscQueue we need to ensure pendingTasks() is only executed from within the EventLoop as
    // otherwise we may see unexpected behavior (as size() is only allowed to be called by a single consumer).
    // See https://github.com/netty/netty/issues/5297
    if (inEventLoop()) {
        return super.pendingTasks();
    } else {
        return submit(pendingTasksCallable).syncUninterruptibly().getNow();
    }
}

因為pendingTasks()方法的底層就是調用taskQueue.size()方法,而前面我們已經說了taskQueue是一個MpscQueue,所以只能由EventLoop所在的線程來調用這個pendingTasks()方法,如果當前線程不是EventLoop所在線程,那么就將pendingTasks()封裝在一個Callable(即,pendingTasksCallable)提交到taskQueue中去執行,并同步的等待執行的結果。

  • 靜態代碼塊
// Workaround for JDK NIO bug.
//
// See:
// - http://bugs.sun.com/view_bug.do?bug_id=6427854
// - https://github.com/netty/netty/issues/203
static {
    final String key = "sun.nio.ch.bugLevel";
    final String buglevel = SystemPropertyUtil.get(key);
    if (buglevel == null) {
        try {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    System.setProperty(key, "");
                    return null;
                }
            });
        } catch (final SecurityException e) {
            logger.debug("Unable to get/set System Property: " + key, e);
        }
    }

    int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
    if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) {
        selectorAutoRebuildThreshold = 0;
    }

    SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;

    if (logger.isDebugEnabled()) {
        logger.debug("-Dio.netty.noKeySetOptimization: {}", DISABLE_KEYSET_OPTIMIZATION);
        logger.debug("-Dio.netty.selectorAutoRebuildThreshold: {}", SELECTOR_AUTO_REBUILD_THRESHOLD);
    }
}

這里的靜態代碼塊,主要做了兩件事:
① 解決在java6 中 NIO Selector.open()可能拋出NPE異常的問題。
問題:http://bugs.java.com/view_bug.do?bug_id=6427854
解決:https://github.com/netty/netty/issues/203
這里我們對問題和Netty的解決方案進行一個簡單的講解。
問題描述:
sun.nio.ch.Util中包含??線程不安全的代碼,并可能拋出一個NullPointerException異常。

異常發生情景:當有兩個線程thread_A和thread_B。thread_A先進入了該方法,并且調用了‘if (bugLevel == null)’,此時bugLevel為null,thread_A進入if塊中,并執行‘bugLevel = (String)AccessController.doPrivileged(pa);’,該調用得到的結果bugLevel依舊為null。此時,thread_B也進入了該方法,并且在thread_A調用‘bugLevel = "”’之前,執行了‘if (bugLevel == null)’并進入了if塊。然后thread_A繼續執行,在執行‘return (bugLevel != null) && bugLevel.equals(bl);’時,thread_A先判斷了(bugLevel != null),此時為true。但在thread_A執行bugLevel.equals(bl)之前,thread_B執行了‘bugLevel = (String)AccessController.doPrivileged(pa);’,這將導致bugLevel并重新置為了null,那么如果thread_A在thread_B調用‘bugLevel = "";’之前先去調用了‘bugLevel.equals(bl)’,那么就會使得thread_A拋出一個NullPointerException異常。

正是因為

java.security.PrivilegedAction pa =
                new GetPropertyAction("sun.nio.ch.bugLevel");
// the next line can reset bugLevel to null
            bugLevel = (String)AccessController.doPrivileged(pa);

??的調用導致bugLevel又被重置為了null。導致了NPE bug的發生。

Netty的解決方案:在開始使用Selector.open()方法之前,先將"sun.nio.ch.bugLevel"系統屬性設置為non-null的。即,如果"sun.nio.ch.bugLevel”系統屬性值為null,則設置”sun.nio.ch.bugLevel”=“”

② 為了在事件循環時解決JDK NIO類庫的epoll bug,先設置好SELECTOR_AUTO_REBUILD_THRESHOLD,即selector空輪詢的閾值。具體的賦值流程上面已經詳細說明過了。

  • 喚醒select標識符
// 一個原子類的Boolean標識用于控制決定一個阻塞著的Selector.select是否應該結束它的選擇操作。

private final AtomicBoolean wakenUp = new AtomicBoolean();
  • ioRatio
// 在事件循環中期待用于處理I/O操作時間的百分比。默認為50%。
// 也就是說,在事件循環中默認情況下用于處理I/O操作的時間和用于處理任務的時間百分比都為50%,
// 即,用于處理I/O操作的時間和用于處理任務的時間時一樣的。用戶可以根據實際情況來修改這個比率。
private volatile int ioRatio = 50


方法

構造方法

NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
             SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
    super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
    if (selectorProvider == null) {
        throw new NullPointerException("selectorProvider");
    }
    if (strategy == null) {
        throw new NullPointerException("selectStrategy");
    }
    provider = selectorProvider;
    final SelectorTuple selectorTuple = openSelector();
    selector = selectorTuple.selector;
    unwrappedSelector = selectorTuple.unwrappedSelector;
    selectStrategy = strategy;
}

a) 完成成員屬性taskQueue、tailQueue的構建;
最終會調用newTaskQueue方法來完成構建:

    protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
        // This event loop never calls takeTask()
        return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.<Runnable>newMpscQueue()
                                                    : PlatformDependent.<Runnable>newMpscQueue(maxPendingTasks);
    }

參數maxPendingTasks默認為Integer.MAX_VALUE,則會通過“PlatformDependent.<Runnable>newMpscQueue()”返回來構造一個MpscUnboundedArrayQueue實例,其初容量大小為1024,最大容量限制為2048。
b) 設置成員屬性addTaskWakesUp為false。
c) 設置成員屬性rejectedExecutionHandler值為RejectedExecutionHandlers.reject()方法將返回一個RejectedExecutionHandler實例。

/**
 * Similar to {@link java.util.concurrent.RejectedExecutionHandler} but specific to {@link SingleThreadEventExecutor}.
 */
public interface RejectedExecutionHandler {

    /**
     * Called when someone tried to add a task to {@link SingleThreadEventExecutor} but this failed due capacity
     * restrictions.
     */
    void rejected(Runnable task, SingleThreadEventExecutor executor);
}

RejectedExecutionHandler接口類似于JDK 的 java.util.concurrent.RejectedExecutionHandler,但是RejectedExecutionHandler只針對于SingleThreadEventExecutor。
該接口中有一個唯一的接口方法rejected,當嘗試去添加一個任務到SingleThreadEventExecutor中,但是由于容量的限制添加失敗了,那么此時該方法就會被調用。
RejectedExecutionHandlers.reject()返回的是一個RejectedExecutionHandler常量REJECT

    private static final RejectedExecutionHandler REJECT = new RejectedExecutionHandler() {
        @Override
        public void rejected(Runnable task, SingleThreadEventExecutor executor) {
            throw new RejectedExecutionException();
        }
    };

該RejectedExecutionHandler總是拋出一個RejectedExecutionException異常。
d) final SelectorTuple selectorTuple = openSelector();
開啟Selector,構造SelectorTuple實例,SelectorTuple是一個封裝了原始selector對象和封裝后selector對象(即,SelectedSelectionKeySetSelector對象)的類:

private static final class SelectorTuple {
    final Selector unwrappedSelector;
    final Selector selector;

這里,成員變量unwrappedSelector就是通過SelectorProvider.provider().openSelector()開啟的Selector;而成員變量selector則是一個SelectedSelectionKeySetSelector對象。
SelectedSelectionKeySetSelector中持有unwrappedSelector實例,并作為unwrappedSelector的代理類,提供Selector所需要的方法,而Selector相關的操作底層實際上都是由unwrappedSelector來完成的,只是在操作中增加了對selectionKeys進行相應的設置。SelectedSelectionKeySetSelector中除了持有unwrappedSelector實例外還持有一個SelectedSelectionKeySet對象。該對象是Netty提供的一個可以‘代替’Selector selectedKeys的對象。openSelector()方法中通過反射機制將程序構建的SelectedSelectionKeySet對象給設置到了Selector內部的selectedKeys、publicSelectedKeys屬性。這使Selector中所有對selectedKeys、publicSelectedKeys的操作實際上就是對SelectedSelectionKeySet的操作。
SelectedSelectionKeySet類主要通過成員變量SelectionKey[]數組來維護被選擇的SelectionKeys,并將擴容操作簡單的簡化為了’newCapacity為oldCapacity的2倍’來實現。同時不在支持remove、contains、iterator方法。并添加了reset方法來對SelectionKey[]數組進行重置。
SelectedSelectionKeySetSelector中主要是在每次select操作的時候,都會先將selectedKeys進行清除(reset)操作。
e) 設置成員屬性selectStrategy的值為DefaultSelectStrategyFactory.INSTANCE.newSelectStrategy(),即一個DefaultSelectStrategy實例。

事件循環


NioEventLoop的事件循環主要完成下面幾件事:
① 根據當前NioEventLoop中是否有待完成的任務得出select策略,進行相應的select操作
② 處理select操作得到的已經準備好處理的I/O事件,以及處理提交到當前EventLoop的任務(包括定時和周期任務)。
③ 如果NioEventLoop所在線程執行了關閉操作,則執行相關的關閉操作處理。這一塊在之前Netty 源碼解析 ——— Netty 優雅關閉流程的文章已經做了詳細的說明,這里就不再贅述了。

下面我們詳細展開每一步

① 根據當前NioEventLoop中是否有待完成的任務得出select策略,進行相應的select操作:

『selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())』
前面我們已經說過selectNowSupplier是一個selectNow提供器:

    // NioEventLoop#selectNowSupplier
    private final IntSupplier selectNowSupplier = new IntSupplier() {
        @Override
        public int get() throws Exception {
            return selectNow();
        }
    };

    // NioEventLoop#selectNow()
    int selectNow() throws IOException {
        try {
            return selector.selectNow();
        } finally {
            // restore wakeup state if needed
            if (wakenUp.get()) {
                selector.wakeup();
            }
        }
    }

    // SelectedSelectionKeySetSelector#selectNow() 
    public int selectNow() throws IOException {
        selectionKeys.reset();
        return delegate.selectNow();
    }

    //  SelectedSelectionKeySetSelector#wakeup()
    public Selector wakeup() {
        return delegate.wakeup();
    }

selectNowSupplier提供的selectNow()操作是通過封裝過的selector(即,SelectedSelectionKeySetSelector對象)來完成的。而SelectedSelectionKeySetSelector的selectorNow()方法處理委托真實的selector完成selectoNow()操作外,還會將selectionKeys清空。

hasTasks()方法用于判斷taskQueue或tailTasks中是否有任務。

前面我們也提到過selectStrategy就是一個DefaultSelectStrategy對象:

final class DefaultSelectStrategy implements SelectStrategy {
    static final SelectStrategy INSTANCE = new DefaultSelectStrategy();

    private DefaultSelectStrategy() { }

    @Override
    public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
        return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
    }
}

DefaultSelectStrategy的選擇策略就是:
如果當前的EventLoop中有待處理的任務,那么會調用selectSupplier.get()方法,也就是最終會調用Selector.selectNow()方法,并清空selectionKeys。Selector.selectNow()方法不會發生阻塞,如果沒有一個channel(即,該channel注冊的事件發生了)被選擇也會立即返回,否則返回就緒I/O事件的個數。
如果當前的EventLoop中沒有待處理的任務,那么返回’SelectStrategy.SELECT(即,-1)’。

如果‘selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())’操作返回的是一個>0的值,則說明有就緒的的I/O事件待處理,則直接進入流程②。否則,如果返回的是’SelectStrategy.SELECT’則進行select(wakenUp.getAndSet(false))操作:
首先先通過自旋鎖(自旋 + CAS)方式獲得wakenUp當前的標識,并再將wakenUp標識設置為false。將wakenUp作為參數傳入select(boolean oldWakenUp)方法中,注意這個select方法不是JDK NIO的Selector.select方法,是NioEventLoop類自己實現的一個方法,只是方法名一樣而已。NioEventLoop的這個select方法還做了一件很重要的時,就是解決“JDK NIO類庫的epoll bug”問題。

  • 解決 JDK NIO 類庫的 epool bug
    下面我們來對這個“JDK NIO類庫的epoll bug”問題已經Netty是如何解決這個問題進行一個說明:
    JDK NIO類庫最著名的就是 epoll bug了,它會導致Selector空輪詢,IO線程CPU 100%,嚴重影響系統的安全性和可靠性。

SUN在解決該BUG的問題上不給力,只能從NIO框架層面進行問題規避,下面我們看下Netty是如何解決該問題的。

Netty的解決策略:

  1. 根據該BUG的特征,首先偵測該BUG是否發生;
  2. 將問題Selector上注冊的Channel轉移到新建的Selector上;
  3. 老的問題Selector關閉,使用新建的Selector替換。

下面具體看下代碼,首先檢測是否發生了該BUG:



紅色框中的代碼,主要完成了是否發生“epoll-bug”的檢測。
『if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos)』返回false,即『time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) < currentTimeNanos』 的意思是:int selectedKeys = selector.select(timeoutMillis)在timeoutMillis時間到期前就返回了,并且selectedKeys==0,則說明selector進行了一次空輪詢,這違反了Javadoc中對Selector.select(timeout)方法的描述。epoll-bug會導致無效的狀態選擇和100%的CPU利用率。也就是Selector不管有無感興趣的事件發生,select總是不阻塞就返回。這會導致select方法總是無效的被調用然后立即返回,依次不斷的進行空輪詢,導致CPU的利用率達到了100%。

int selectorAutoRebuildThreshold = SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512);
if (selectorAutoRebuildThreshold < MIN_PREMATURE_SELECTOR_RETURNS) {
    selectorAutoRebuildThreshold = 0;
}

SELECTOR_AUTO_REBUILD_THRESHOLD = selectorAutoRebuildThreshold;

SELECTOR_AUTO_REBUILD_THRESHOLD默認為512,也就是當Selector連續執行了512次空輪詢后,Netty就會進行Selector的重建操作,即rebuildSelector()操作。

綠色框中代碼主要說明了,當有定時/周期性任務即將到達執行時間(<0.5ms),或者NioEventLoop的線程收到了新提交的任務上來等待著被處理,或者有定時/周期性任務到達了可處理狀態等待被處理,那么則退出select方法轉而去執行任務。這也說明Netty總是會盡最大努力去保證任務隊列中的任務以及定時/周期性任務能得到及時的處理。

long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
    if (selectCnt == 0) {
        selector.selectNow();
        selectCnt = 1;
    }
    break;
}

該段代碼會計算scheduledTaskQueue中是否有即將要執行的任務,即在0.5ms內就可執行的scheduledTask,如果有則退出select方法轉而去執行任務。
‘selectDeadLineNanos’的初始值通過‘currentTimeNanos + delayNanos(currentTimeNanos);’而來。delayNanos方法會返回最近一個待執行的定時/周期性任務還差多少納秒就可以執行的時間差(若,scheduledTaskQueue為空,也就是沒有任務的定時/周期性任務,則返回1秒)。因此selectDeadLineNanos就表示最近一個待執行的定時/周期性任務的可執行時間。
‘selectDeadLineNanos - currentTimeNanos’就表示:最近一個待執行的定時/周期性任務還差多少納秒就可以執行的時間差。我們用scheduledTaskDelayNanos來表示該差值。
‘(selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L’表示:(scheduledTaskDelayNanos + 0.5ms) / 1ms。如果該結果大于0,則說明scheduledTaskDelayNanos >= 0.5ms,否則scheduledTaskDelayNanos < 0.5ms。
因此,就有了上面所說的結論,scheduledTaskQueue中有在0.5ms內就可執行的任務,則退出select方法轉而去執行任務。

// If a task was submitted when wakenUp value was true, the task didn't get a chance to call
// Selector#wakeup. So we need to check task queue again before executing select operation.
// If we don't, the task might be pended until select operation was timed out.
// It might be pended until idle timeout if IdleStateHandler existed in pipeline.
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
    selector.selectNow();
    selectCnt = 1;
    break;
}

在了解??代碼的用意之前,我們先來說下,當有任務提交至EventLoop時的一些細節補充

Netty 源碼解析 ——— 服務端啟動流程 (上)中我們已經對任務提交至taskQueue做了介紹,這里我們補充說明的是,當一個非EventLoop線程提交了一個任務到EventLoop的taskQueue后,在什么情況下會去觸發Selector.wakeup()。當滿足下面4個條件時,在有任務提交至EventLoop后會觸發Selector的wakeup()方法:
a) 成員變量addTaskWakesUp為false。
這里,在構造NioEventLoop對象時,通過構造方法傳進的參數’addTaskWakesUp’正是false,它會賦值給成員變量addTaskWakesUp。因此該條件滿足。
b)當提交上來的任務不是一個NonWakeupRunnable任務

    // NioEventLoop#wakeup(boolean inEventLoop)
    protected void wakeup(boolean inEventLoop) {
        if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
            selector.wakeup();
        }
    }

c) 執行提交任務的線程不是EventLoop所在線程
d) 當wakenUp成員變量當前的值為false

    // NioEventLoop#wakeup(boolean inEventLoop)
    protected void wakeup(boolean inEventLoop) {
        if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
            selector.wakeup();
        }
    }

只有同時滿足上面4個條件的情況下,Selector的wakeup()方法才會的以調用。

現在,我們來說明這段代碼塊的用意

// If a task was submitted when wakenUp value was true, the task didn't get a chance to call
// Selector#wakeup. So we need to check task queue again before executing select operation.
// If we don't, the task might be pended until select operation was timed out.
// It might be pended until idle timeout if IdleStateHandler existed in pipeline.
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
    selector.selectNow();
    selectCnt = 1;
    break;
}

如果一個任務在wakenUp值為true的情況下被提交上來,那么這個任務將沒有機會去調用Selector.wakeup()(即,此時’d)’條件不滿足)。所以我們需要去再次檢測任務隊列中是否有待執行的任務,在執行Selector.select操作之前。如果我們不這么做,那么任務隊列中的任務將等待直到Selector.select操作超時。如果ChannelPipeline中存在IdleStateHandler,那么IdleStateHandler處理器可能會被掛起直到空閑超時。
首先,這段代碼是在每次要執行Selector.select(long timeout)之前我們會進行一個判斷。我們能夠確定的事,如果hasTasks()為true,即發現當前有任務待處理時。wakenUp.compareAndSet(false, true)會返回true,因為在每次調用當前這個select方法時,都會將wakenUp標識設置為false(即,‘wakenUp.getAndSet(false)’這句代碼)。而此時,wakenUp已經被置位true了,在此之后有任務提交至EventLoop,那么是無法觸發Selector.wakeup()的。所以如果當前有待處理的任務,就不會進行下面的Selector.select(long timeout)操作,而是退出select方法,繼而去處理任務。
因為如果不這么做的話,如果當前NioEventLoop線程上已經有任務提交上來,這會使得這些任務可能會需要等待Selector.select(long timeout)操作超時后才能得以執行。再者,假設我們的ChannelPipeline中存在一個IdleStateHandler,那么就可能導致因為Selector.select(long timeout)操作的timeout比IdleStateHandler設置的idle timeout長,而導致IdleStateHandler不能對空閑超時做出即使的處理。
同時,我們注意,在執行‘break’退出select方法前,會執行‘selector.selectNow()’,該方法不會阻塞,它會立即返回,同時它會抵消Selector.wakeup()操作帶來的影響(關于NIO 相關的知識點,歡迎參閱關于 NIO 你不得不知道的一些“地雷”)。

所以,① 如有有非NioEventLoop線程提交了一個任務上來,那么這個線程會執行『selector
.wakeup()』方法,那么NioEventLoop在『if (hasTasks() && wakenUp.compareAndSet(false, true))』的后半個條件會返回false,程序會執行到『int selectedKeys = selector.select(timeoutMillis);』,但是此時select不會阻塞,而是直接返回,因為前面已經先執行了『selector.wakeup()』;② 因為提交任務的線程是非NioEventLoop線程,所以也可能是由NioEventLoop線程成功執行了『if (hasTasks() && wakenUp.compareAndSet(false, true))』,退出了select方法轉而去執行任務隊列中的任務。注意,這是提交任務的非NioEventLoop線程就不會執行『selector.wakeup()』。

if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
    // - Selected something,
    // - waken up by user, or
    // - the task queue has a pending task.
    // - a scheduled task is ready for processing
    break;
}

同時,除了在每次Selector.select(long timeout)操作前進行任務隊列的檢測外,在每次Selector.select(long timeout)操作后也會檢測任務隊列是否已經有提交上來的任務待處理,以及是由有定時或周期性任務準備好被執行。如果有,也不會繼續“epoll-bug”的檢測,轉而去執行待處理的任務。

好了,我們在來看下如果經過檢測,我們已經確認發生了“epoll-bug”,這時我們就需要進行Selector的重構操作:

    private void rebuildSelector0() {
        final Selector oldSelector = selector;
        final SelectorTuple newSelectorTuple;

        if (oldSelector == null) {
            return;
        }

        try {
            newSelectorTuple = openSelector();
        } catch (Exception e) {
            logger.warn("Failed to create a new Selector.", e);
            return;
        }

        // Register all channels to the new Selector.
        int nChannels = 0;
        for (SelectionKey key: oldSelector.keys()) {
            Object a = key.attachment();
            try {
                if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                    continue;
                }

                int interestOps = key.interestOps();
                key.cancel();
                SelectionKey newKey = key.channel().register(newSelectorTuple.unwrappedSelector, interestOps, a);
                if (a instanceof AbstractNioChannel) {
                    // Update SelectionKey
                    ((AbstractNioChannel) a).selectionKey = newKey;
                }
                nChannels ++;
            } catch (Exception e) {
                logger.warn("Failed to re-register a Channel to the new Selector.", e);
                if (a instanceof AbstractNioChannel) {
                    AbstractNioChannel ch = (AbstractNioChannel) a;
                    ch.unsafe().close(ch.unsafe().voidPromise());
                } else {
                    @SuppressWarnings("unchecked")
                    NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
                    invokeChannelUnregistered(task, key, e);
                }
            }
        }

        selector = newSelectorTuple.selector;
        unwrappedSelector = newSelectorTuple.unwrappedSelector;

        try {
            // time to close the old selector as everything else is registered to the new one
            oldSelector.close();
        } catch (Throwable t) {
            if (logger.isWarnEnabled()) {
                logger.warn("Failed to close the old Selector.", t);
            }
        }

        logger.info("Migrated " + nChannels + " channel(s) to the new Selector.");
    }

重構操作主要的流程:首先,通過openSelector()先構造一個新的SelectorTuple。然后,遍歷oldSelector中的所有SelectionKey,依次判斷其有效性,如果有效則將其重新注冊到新的Selector上,并將舊的SelectionKey執行cancel操作,進行相關的數據清理,以便最后oldSelector好進行關閉。在將所有的SelectionKey數據移至新的Selector后,將newSelectorTuple的selector和unwrappedSelector賦值給相應的成員屬性。最后,調用oldSelector.close()關閉舊的Selector以進行資源的釋放。


接下我們繼續討論NioEventLoop.select操作的流程②

② 處理select操作得到的已經準備好處理的I/O事件,以及處理提交到當前EventLoop的任務(包括定時和周期任務):


a) 首先先將成員變量cancelledKeys和needsToSelectAgain重置,即,cancelledKeys置為0,needsToSelectAgain置為false;
b) 成員變量ioRatio的默認值為50
private volatile int ioRatio = 50;
ioRatio在事件循環中期待用于處理I/O操作時間的百分比。默認為50%。也就是說,在事件循環中默認情況下用于處理I/O操作的時間和用于處理任務的時間百分比都為50%,即,用于處理I/O操作的時間和用于處理任務的時間時一樣的。
這里做個簡單的證明吧:
當ioRatio不為100%時,我們假設在事件循環中用于處理任務時間的百分比為taskRatio,I/O操作的時間為ioTime,處理任務的時間為taskTime,求taskTime:
ioTime/taskTime = ioRatio/taskRatio; 并且 ioRatio + taskRatio = 100;
帶入,ioTime/taskTime = ioRatio/(100-ioRatio); ==> taskTime = ioTime*(100 - ioRatio) / ioRatio;
所以runAllTasks(ioTime * (100 - ioRatio) / ioRatio);傳入的參數就為可用于運行任務的時間。
c) processSelectedKeys():處理Selector.select操作返回的待處理的I/O事件。

注意,『selectedKeys.keys[i] = null;』操作相當于我們在NIO編程中在處理已經觸發的感興趣的事件時,要將處理過的事件充selectedKeys集合中移除的步驟。
該方法會從selectedKeys中依次取出準備好被處理的SelectionKey,并對相應的待處理的I/O事件進行處理。
Netty 源碼解析 ——— 服務端啟動流程 (下)說到過,再將ServerSocketChannel注冊到Selector的時候,是會將其對應的NioServerSocketChannel作為附加屬性設置到SelectionKey中。所有這里從k.attachment()獲取到的Object對象實際就是NioServerSocketChannel,而NioServerSocketChannel就是一個AbstractNioChannel的實現類。
首先檢查當前的SelectionKey是否有效(僅當SelectionKey從Selector上注銷的時候,該SelectionKey會為無效狀態),如果無效的話:a) 獲取該SelectionKey所關聯的Channel所注冊的EventLoop,如果獲取Channel的EventLoop失敗,則忽略錯誤直接返回。因為我們只處理注冊到EventLoop上的Channel且有權去關閉這個Channel;b) 如果獲取到的EventLoop不是當前的執行線程所綁定的EventLoop,或者獲取到的EventLoop為null,則直接返回。因為我們只關注依然注冊在當前執行線程所綁定的EventLoop上的Channel。Channel可能已經從當前的EventLoop上注銷了,并且它的SelectionKey可能已經被取消了,作為在注銷處理流程的一部分。當然,如果Channel仍然健康的被注冊在當前的EventLoop上,則需要去關閉它;c) 當能正確獲取到EventLoop,且該EventLoop非空并未當前執行線程所綁定的EventLoop,則說明Channel依舊注冊去當前的EventLoop上,那么執行關閉操作,來關閉相應的連接,釋放相應的資源。

當SelectionKey.OP_CONNECT(連接事件)準備就緒時,我們執行如下操作:

    if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
        // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
        // See https://github.com/netty/netty/issues/924
        int ops = k.interestOps();
        ops &= ~SelectionKey.OP_CONNECT;
        k.interestOps(ops);

        unsafe.finishConnect();
    }

將SelectionKey.OP_CONNECT事件從SelectionKey所感興趣的事件中移除,這樣Selector就不會再去監聽該連接的SelectionKey.OP_CONNECT事件了。而SelectionKey.OP_CONNECT連接事件是只需要處理一次的事件,一旦連接建立完成,就可以進行讀、寫操作了。
unsafe.finishConnect():該方法會調用SocketChannel.finishConnect()來標識連接的完成,如果我們不調用該方法,就去調用read/write方法,則會拋出一個NotYetConnectedException異常。在此之后,觸發ChannelActive事件,該事件會在該Channel的ChannelPipeline中傳播處理。

具體的關于SelectionKey.OP_CONNECT、SelectionKey.OP_WRITE、SelectionKey.OP_READ、SelectionKey.OP_ACCEPT的處理流程可以參閱Netty 源碼解析 ——— 基于 NIO 網絡傳輸模式的 OP_ACCEPT、OP_CONNECT、OP_READ、OP_WRITE 事件處理流程

d) 處理任務隊列中的任務以及定時/周期性任務。

    try {
        processSelectedKeys();
    } finally {
        // Ensure we always run tasks.
        final long ioTime = System.nanoTime() - ioStartTime;
        runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
    }

首先,這里將執行任務的語句寫在了finally塊中,這是為了確保即便處理SelectedKeys出現了異常,也要確保任務中的隊列總能得到執行的機會。

上面我們已經說了,默認情況下用于處理任務的時間和處理I/O操作所需的時間時一樣的。那么接下來我們來看看runAllTasks方法:

① 獲取系統啟動到當前的時間內已經過去的定時任務(即,延遲的時間已經滿足或者定時執行任務的時間已經滿足的任務)放入到taskQueue中。

從taskQueue中獲取任務,如果taskQueue已經沒有任務了,則依次執行tailTasks隊列里的所有任務。
a) 『fetchFromScheduledTaskQueue()』

    // SingleThreadEventExecutor#fetchFromScheduledTaskQueue()
    private boolean fetchFromScheduledTaskQueue() {
        long nanoTime = AbstractScheduledEventExecutor.nanoTime();
        Runnable scheduledTask  = pollScheduledTask(nanoTime);
        while (scheduledTask != null) {
            if (!taskQueue.offer(scheduledTask)) {
                // No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
                scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
                return false;
            }
            scheduledTask  = pollScheduledTask(nanoTime);
        }
        return true;
    }

獲取從系統啟動到當前系統的時間間隔。從scheduledTaskQueue中獲取在該時間間隔內已經過期的任務(即延遲周期或定時周期已經到時間的任務),將這些任務放入到taskQueue中,如果taskQueue滿了無法進入添加新的任務(taskQueue隊列的容量限制最大為2048),則將其重新放回到scheduledTaskQueue。
默認情況下,taskQueue是一個MpscUnboundedArrayQueue實例。

    // AbstractScheduledEventExecutor#pollScheduledTask(long nanoTime)
    protected final Runnable pollScheduledTask(long nanoTime) {
        assert inEventLoop();

        Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
        ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
        if (scheduledTask == null) {
            return null;
        }

        if (scheduledTask.deadlineNanos() <= nanoTime) {
            scheduledTaskQueue.remove();
            return scheduledTask;
        }
        return null;
    }

根據給定的nanoTime返回已經準備好被執行的Runnable。你必須是用AbstractScheduledEventExecutor的nanoTime()方法來檢索正確的nanoTime。
scheduledTaskQueue是一個PriorityQueue實例,它根據任務的deadlineNanos屬性的升序來維護一個任務隊列,每次peek能返回最先該被執行的定時任務。deadlineNanos表示系統啟動到該任務應該被執行的時間點的時間差,如果“scheduledTask.deadlineNanos() <= nanoTime”則說明該任務的執行時間已經到了,因此將其從scheduledTaskQueue移除,然后通過該方法返回后放入到taskQueue中等待被執行。
因此,可知每次執行taskQueue前,taskQueue中除了有用戶自定義提交的任務,系統邏輯流程提交至該NioEventLoop的任務,還有用戶自定義或者系統設置的已經達到運行時間點的定時/周期性任務會一并放入到taskQueue中,而taskQueue的初始化容量為1024,最大長度限制為2048,也就是一次事件循環最多只能處理2048個任務。
b) 然后從taskQueue中獲取一個待執行的任務,如果獲取的task為null,說明本次事件循環中沒有任何待執行的任何,那么就執行“afterRunningAllTasks()”后返回。afterRunningAllTasks()方法會依次執行tailQueue中的任務,tailTasks中是用戶自定義的一些列在本次事件循環遍歷結束后會執行的任務,你可以通過類似如下的方式來添加tailTask:

((NioEventLoop)ctx.channel().eventLoop()).executeAfterEventLoopIteration(() -> {
    // add some task to execute after eventLoop iteration
});

② 通過“系統啟動到當前的時間差”+“可用于執行任務的時間”=“系統啟動到可用于執行任務時間的時間段(deadline)”。從taskQueue中依次出去任務,如果task為null則說明已經沒有待執行的任務,那么退出for循環。否則,同步的執行task,每執行64個任務后,就計算“系統啟動到當前的時間”是否大于等于了deadline,如果是則說明已經超過了分配給任務執行的時間,此時就不會繼續執行taskQueue中的任務了。
a)『safeExecute(task)』

    protected static void safeExecute(Runnable task) {
        try {
            task.run();
        } catch (Throwable t) {
            logger.warn("A task raised an exception. Task: {}", task, t);
        }
    }

通過直接調用task的run方法來同步的執行任務。
b) 『runTasks & 0x3f』:

// Check timeout every 64 tasks because nanoTime() is relatively expensive.
// XXX: Hard-coded value - will make it configurable if it is really a problem.
if ((runTasks & 0x3F) == 0) {
    lastExecutionTime = ScheduledFutureTask.nanoTime();
    if (lastExecutionTime >= deadline) {
        break;
    }
}

63的16進制表示為0x3f(二進制表示為’0011 1111’),當已經執行的任務數量小于64時,其與0x3f的位與操作會大于0,當其等于64(64的16進制表示為0x40,二進制表示為’0100 0000’)時,runTasks & 0x3f的結果為0。所以是每執行64個任務后就進行一次時間的判斷,以保證執行任務隊列的任務不會嚴重的超過我們所設定的時間。

③ 則依次執行tailTasks隊列里的所有任務。賦值全局屬性lastExecutionTime為最后一個任務執行完后的時間。

到此為止,整個事件循環的流程就已經分析完了。

后記

若文章有任何錯誤,望大家不吝指教:)

參考

http://www.infoq.com/cn/articles/netty-reliability
《Java 并發編程的藝術》

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容