Netty學習筆記(五)NioEventLoop啟動

weixin_34402408發表於2018-11-10

引子

在之前的文章中有提到BootStrap啟動類中繫結埠的內部實現如下:

    private static void doBind0(
            final ChannelFuture regFuture, final Channel channel,
            final SocketAddress localAddress, final ChannelPromise promise) {

        channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });
    }

之前只提到了 channel.bind方法繫結埠,但是未提到channel.eventLoop().execute()方法的內部實現。在執行過程中,channel.eventLoop()實際就是NioEventLoop物件,我們首先就來看它是如何啟動執行緒的。

NioEventLoop.execute()

它的execute方法實現如下:

    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方法判斷當前呼叫執行緒是否為NioEventLoop執行緒,由於當前是main主執行緒,並且NioEventLoop執行緒還未建立,所以inEventLoop()方法返回false,會執行startThread方法。這個startThread方法最終實現如下:

    private void doStartThread() {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                ......
                    SingleThreadEventExecutor.this.run();
               ......
            }
    }

可以看出這個方法做的就是呼叫執行緒池物件的execute方法建立執行緒,原始碼中的executor物件就是在分析NioEventLoopGroup那篇文章中說到的ThreadPerTaskExecutor執行緒池物件。
另外執行過程中SingleThreadEventExecutor.this實際就是NioEventLoop物件,它的run方法裡面封裝了啟動過程,這部分內容將在下面繼續闡述。

NioEventLoop.run()

在這個run方法中主要做了下面幾件事:

  • select() 檢查是否有IO事件
  • processSelectedKeys() 處理IO事件
  • runAllTasks() 處理非同步任務佇列

下面就這幾個方面來看下原始碼是如何實現的。

select() 檢查是否有IO事件:

    @Override
    protected void run() {
        for (;;) {
            try {
                switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));
                    ......

可以看出run方法中首先寫了個死迴圈進行輪詢。在除錯過程中發現switch預設走的是SelectStrategy.SELECT分支,即執行select(wakenUp.getAndSet(false));,這個方法實現分為下面幾個方面:

  1. 設定deadline超時時間
  2. 阻塞式select
  3. 避免jdk空輪詢的bug
    private void select(boolean oldWakenUp) throws IOException {
        Selector selector = this.selector;
        try {
            int selectCnt = 0;
            long currentTimeNanos = System.nanoTime();
            long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
            for (;;) {
                long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
                if (timeoutMillis <= 0) {
                    if (selectCnt == 0) {
                        selector.selectNow(); // part: 1
                        selectCnt = 1;
                    }
                    break;
                }
                ......
                int selectedKeys = selector.select(timeoutMillis); // part: 2
                selectCnt ++;

                if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
                    break;
                }

在select方法內部又是一個沒有條件的for迴圈。首先計算select操作允許的超時時間timeoutMillis和select操作的截止時間selectDeadLineNanos。在for迴圈中,如果當前迴圈的時間已經超過截止時間,則執行選擇器的selector.selectNow();方法,selectNow方法是非阻塞的,並且直接跳出for迴圈,此次select操作就結束了,這就是第一部分:設定deadline超時時間,並進行非阻塞select呼叫。
第二部分就是當前時間沒有超時的情況下執行選擇器的select方法進行阻塞呼叫。select方法返回selectedKeys不為0的時候表示選擇器已經選好了通道,跳出迴圈,本次select操作也就結束了。
關於第三部分netty是如何避免jdk空輪詢的bug的,還是不懂,這裡就先預留了個問題了。。。

processSelectedKeys() 處理IO事件
處理IO事件主要分為兩個部分:selected keySet優化和processSelectedKeys()方法的執行。

  1. selected keySet優化
    在我們例項化EventLoop物件,執行建構函式的時候,呼叫了selector = openSelector();,在這個方法內部建立了一個selectedKeySet 物件:final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();那這個selectedKeySet是種什麼型別呢:
final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
    private SelectionKey[] keysA;
    private int keysASize;
    SelectedSelectionKeySet() {
        keysA = new SelectionKey[1024];
    }
......

檢視原始碼可知,這個繼承了AbstractSet的物件,實際並不是用Set型別存放SelectionKey,而是用陣列來實現的。這樣做的目的是為了降低時間複雜度。

  1. 執行processSelectedKeys()方法
private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized(selectedKeys.flip());
        } else {
            processSelectedKeysPlain(selector.selectedKeys());
        }
    }

上述方法中,當selectedKeys不為空時,執行的processSelectedKeysOptimized方法程式碼如下:

    private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
        for (int i = 0;; i ++) {
            final SelectionKey k = selectedKeys[i];
            if (k == null) {
                break;
            }
            selectedKeys[i] = null;
            final Object a = k.attachment();
            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);
            } 
            ......  
        }
    }

可以看到在這個方法中開始遍歷selectedKeys這個陣列,並通過SelectionKey獲取繫結的attachment屬性,而這個屬性實際就是NioSocketChannel物件,所以接著執行processSelectedKey()這個方法:

    private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
            int readyOps = k.readyOps();
            if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                int ops = k.interestOps();
                ops &= ~SelectionKey.OP_CONNECT;
                k.interestOps(ops);
                unsafe.finishConnect();
            }
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                ch.unsafe().forceFlush();
            }
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
                if (!ch.isOpen()) {
                    return;
                }
            }
    }

可以看到在這個函式內部最終實現的就是處理各種IO事件的邏輯。程式碼中的readyOps這個準備好的操作與
SelectionKey.OP_WRITE進行與操作,即位運算,這個在微控制器程式設計中比較常見,最終使用NioSocketChannel的unsafe()方法獲取unsafe物件,資料讀寫都是通過這個unsafe物件來實現的。可以想象這個unsafe物件肯定實現了最終寫到緩衝區ButeBuf的操作,並且還和pipeline進行有關聯,方便事件和資料在各個ChannelHandler之間進行傳遞。關於這個unsafe物件的分析,我們以後繼續。

runAllTasks() 處理非同步任務佇列

  1. task的分類和新增
    在Netty內部有兩個任務佇列,一個是普通佇列,另外一個是定時任務佇列。在NioEventLoop的建構函式中:
    NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,
                 SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, executor, false, DEFAULT_MAX_PENDING_TASKS, rejectedExecutionHandler);
       ......
    }

呼叫了父類SingleThreadEventLoop的建構函式:

    protected SingleThreadEventLoop(EventLoopGroup parent, ThreadFactory threadFactory,
                                    boolean addTaskWakesUp, int maxPendingTasks,
                                    RejectedExecutionHandler rejectedExecutionHandler) {
        super(parent, threadFactory, addTaskWakesUp, maxPendingTasks, rejectedExecutionHandler);
        tailTasks = newTaskQueue(maxPendingTasks);
    }

建構函式中執行newTaskQueue()方法建立一個LinkedBlockingQueue任務佇列:

    protected Queue<Runnable> newTaskQueue(int maxPendingTasks) {
        return new LinkedBlockingQueue<Runnable>(maxPendingTasks);
    }

分析到這可以得知在NioEventLoop物件初始化的時候建立了一個LinkedBlockingQueue型別的任務佇列,下面再回過頭來看下NioEventLoop執行execute方法的時候是如何處理這個佇列的,檢視addTask(task)方法程式碼:

    protected void addTask(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }
        if (!offerTask(task)) {
            reject(task);
        }
    }
    final boolean offerTask(Runnable task) {
        if (isShutdown()) {
            reject();
        }
        return taskQueue.offer(task);
    }

可以看出執行execute方法的時候,就是將傳入的task加到任務佇列裡面去。那這個傳入的task就是我們之前分析的繫結埠時執行NioEventLoop的execute方法時傳入的Runnable介面實現,即將繫結埠這個任務加到任務佇列中。

       channel.eventLoop().execute(new Runnable() {
            @Override
            public void run() {
                if (regFuture.isSuccess()) {
                    channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
                } else {
                    promise.setFailure(regFuture.cause());
                }
            }
        });

分析到這可知NioEventLoop的execute方法執行過程中做了啟動執行緒和將新增繫結任務到任務佇列這兩步,在啟動執行緒後執行的NioEventLoop的run方法這個死迴圈中不斷的執行runAllTasks()方法,這裡面做的就是執行任務的具體過程。

  1. 任務的聚合
    我們接著來分析這個runAllTasks()方法:
    protected boolean runAllTasks() {
        assert inEventLoop();
        boolean fetchedAll;
        boolean ranAtLeastOne = false;

        do {
            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;
    }

看到這個執行所有任務的方法中首先執行的是fetchFromScheduledTaskQueue(),裡面做的是從定時任務佇列裡面獲取第一條任務,並將這條定時任務放到taskQueue裡面。如果由於taskQueue任務佇列空間不足,則重新放回到定時任務佇列中;如果空間足夠的話,就迴圈將所有到當前時間要執行的定時任務全部放到普通任務佇列中。所以這就實現了普通任務佇列和定時任務佇列的聚合。

    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;
    }
  1. 任務的執行
    分析了任務的聚合後,我們再看runAllTasks()方法中的runAllTasksFrom(taskQueue)方法實現:
    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;
            }
        }
    }

這個方法的邏輯就是從任務佇列中迴圈取出所有任務並執行,safeExecute()方法實現:

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

可以看到最終就是執行task.run()方法來執行任務。

相關文章