Netty源码分析:NioEventLoop启动以及其IO操作和Task任务的处理

Netty源码分析:NioEventLoop启动以及其IO操作和Task任务的处理

在上篇博文分析服务端启动的过程中,我们遇到了如下的代码片段,

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new OneTimeTask() {
                        @Override
                        public void run() {
                            register0(promise);//分析
                        }
                    });
                }

以及

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

        // This method is invoked before channelRegistered() is triggered.  Give user handlers a chance to set up
        // the pipeline in its channelRegistered() implementation.
        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 启动以及 EventLoop所负责的IO操作和task任务的处理思路。

NioEventLoop类的execute方法

由于NioEventLoop并没有实现此方法,因此,准确来说,应该是NioEventLoop类的超类SingleThreadEventExecutor中的方法。具体代码如下:

    @Override
    public void execute(Runnable task) {
        if (task == null) {
            throw new NullPointerException("task");
        }

        boolean inEventLoop = inEventLoop();//判断当前线程是否为该NioEventLoop所关联的线程,如果是,则添加任务到任务队列中,如果不是,则先启动线程,然后添加任务到任务队列中去
        if (inEventLoop) {
            addTask(task);
        } else {
            startThread();
            addTask(task);
            //如果发现该线程已经关闭则移除任务并拒绝
            if (isShutdown() && removeTask(task)) {
                reject();
            }
        }

        if (!addTaskWakesUp && wakesUpForTask(task)) {
            wakeup(inEventLoop);
        }
    }

该方法的主要逻辑如下:

1、判断入参数是否为null,如果为null,则抛空指针异常,如果不为null,则进行第2步。

2、通过调用inEventLoop()方法判断当前工作线程是否为该NioEventLoop所关联的线程,如果是,则调用addTask(task)方法将任务添加到任务队列中,如果不是,则进行第3步。

3、启动该NioEventLoop所关联的线程,然后添加任务到任务队列中去 。

4、如果发现该线程已经关闭则移除任务并拒绝

在看startThread()这个方法的具体代码之前,我觉得有必要介绍下EventLoop.execute第一次被调用的地方,因此第一次被调用的地方就是触发startThread()的调用,进而导致了EventLoop所对应的Java线程的启动。

基于这样一个问题,我们开始在服务端启动时开始跟踪代码来寻找。最快的方式是,在EventLoop.execute方法的 startThread();这一行代码打上一个断点,然后看下其调用链即可。

调用链截图如下:

看到了没有,是在服务器端启动的过程中,调用AbstractChannel#AbstractUnsafe.register方法中调用了execute方法,这是因为整个代码都是在主线程中运行的因此在下面的register方法中 eventLoop.inEventLoop() 就为 false, 于是进入到 else 分支, 在这个分支中调用了 eventLoop.execute。eventLoop 是一个 NioEventLoop 的实例, 而 NioEventLoop 没有实现 execute 方法, 因此调用的是 SingleThreadEventExecutor.execute,继而调用startThread方法完成NioEventLoop所对应的线程的启动。

        @Override
        public final void register(EventLoop eventLoop, final ChannelPromise promise) {
           //省略了部分检查代码
            AbstractChannel.this.eventLoop = eventLoop;

            if (eventLoop.inEventLoop()) {
                register0(promise);
            } else {
                try {
                    eventLoop.execute(new OneTimeTask() {
                        @Override
                        public void run() {
                            register0(promise);
                        }
                    });
                } catch (Throwable t) {
                    //省略了部分异常处理代码
                }
            }
        }

结论:当 EventLoop.execute 第一次被调用时, 就会触发 startThread() 的调用, 进而导致了 EventLoop 所对应的 Java 线程的启动。

EventLoop的启动方法:startThread

上面找到了startThread调用的调用链哈,这里看下startThread()方法

 SingleThreadEventExecutor.java

     private void startThread() {
        if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                delayedTaskQueue.add(new ScheduledFutureTask(
                        this, delayedTaskQueue, Executors.callable(new PurgeTask(), null),
                        ScheduledFutureTask.deadlineNanos(SCHEDULE_PURGE_INTERVAL), -SCHEDULE_PURGE_INTERVAL));
                thread.start();
            }
        }
    }

上面startThread方法中的 STATE_UPDATER 是 SingleThreadEventExecutor 内部维护的一个属性, 它的作用是标识当前的 thread 的状态. 在初始的时候, STATE_UPDATER == ST_NOT_STARTED, 因此第一次调用 startThread() 方法时, 就会进入到 if 语句内, 进而调用到 thread.start()完成NioEventLoop所对应的 Java线程的启动.

以上就完成了NioEventLoop所对应的Java线程的启动。那么启动之后,该线程主要用于干什么呢?

这里先说结论:在 Netty 中, 一个 EventLoop 需要负责两个工作, 第一个是作为 IO 线程, 负责相应的 IO 操作; 第二个是作为任务线程, 执行 taskQueue 中的任务.

上面的分析中调用startThread()方法继而调用thread.start()来启动EventLoop 所对应的Java 线程。下面来线程启动具体做了什么哈,也就是看下如下的run方法主要干了什么哈,

        thread = threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                    //...
             SingleThreadEventExecutor.this.run();//调用了NioEventLoop类中的run方法,下面将主要分析
             //...
         });      

上面的源码本来很长很长,上面是精简版本,简单来说,这里我们只关注一行代码: SingleThreadEventExecutor.this.run(),这里就是调用了NioEventLoop的run方法,下面继续跟。

NioEventLoop.run()方法

NioEventLoop类中的run方法的代码如下:

    @Override
    protected void run() {
        for (;;) {
            boolean oldWakenUp = wakenUp.getAndSet(false);
            try {
                // 当有任务时为了保证任务及时执行采用不阻塞的selectNow获取准备好I/O的连接 
                if (hasTasks()) {
                    selectNow();
                } else {
                // 当无任务时采用阻塞等待的方式获取连接  
                    select(oldWakenUp);

                    if (wakenUp.get()) {
                        selector.wakeup();
                    }
                }

                cancelledKeys = 0;
                needsToSelectAgain = false;
                final int ioRatio = this.ioRatio;
                if (ioRatio == 100) {
                    processSelectedKeys();//分析
                    runAllTasks();//分析
                } else {
                    final long ioStartTime = System.nanoTime();

                    processSelectedKeys();

                    final long ioTime = System.nanoTime() - ioStartTime;
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }

                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        break;
                    }
                }
            } catch (Throwable t) {
                logger.warn("Unexpected exception in the selector loop.", t);

                // Prevent possible consecutive immediate failures that lead to
                // excessive CPU consumption.
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    // Ignore.
                }
            }
        }
    }

上面代码的 for(;;) 是一个无条件的死循环, 这里就是 NioEventLoop 事件循环的核心。其基本逻辑为: 首先调用hasTasks()方法判断当前任务队列taskQueue是否为空。有两种情况, 一种是有任务,则调用selectNow,一种是无任务,则调用select(oldWakeUp),这selectNow和select(oldWakeUp)两者都是获取已经准备好的连接,不同的是select(oldWakeUp)会产生阻塞.

为什么会根据taskQueue是否有任务来调用不同的select方法呢?

答案:如前面所说在 Netty 中, 一个 EventLoop 需要负责两个工作, 第一个是作为 IO 线程, 负责相应的 IO 操作; 第二个是作为任务线程, 执行 taskQueue 中的任务。基于EventLoo的职责所在因此当taskQueue无任务的时候Netty可以选择阻塞的等待IO就绪事件,不影响其他的东西(因此此时taskQueue为空,没有任务需要执行,是吧),而当taskQueue有任务的时候如果选择阻塞的等待IO就绪事件,则就导致taskQueue中的任务不能尽快得到执行,是吧,因此当taskQueue有任务的时候就会调用飞阻塞的selectNo方法,以保证taskQueue中的任务可以尽快执行。

    protected boolean hasTasks() {
        assert inEventLoop();
        return !taskQueue.isEmpty();
    }    

1、如果taskQueue不为空,则调用selectNow()方法

selectNow()方法的代码如下:

     void selectNow() throws IOException {
        try {
            selector.selectNow();
        } finally {
            // restore wakup state if needed
            if (wakenUp.get()) {
                selector.wakeup();
            }
        }
    }

该方法调用了selector.selectNow(),这个 selector 字段正是 Java NIO 中的多路复用器 Selector(KQueueSelectorImpl实例)。

selector.selectNow()方法在博文Java NIO 之 Selector(第二部分selector.select()) 有详细的介绍。

selector.select()函数功能:选择一些I/O操作已经准备好的管道。每个管道对应着一个key。这个方法 是一个阻塞的选择操作。当至少有一个通道被选择时才返回。当这个方法被执行时,当前线程是允许被中断的。

selector.selectNow()这个方法与select()的区别在于,是非阻塞的,即当前操作即使没有通道准备好也是立即返回,只是返回的是0,不会阻塞当前线程.

selectNow()方法在KQueueSelectorImpl类的父类中。

    public int selectNow() throws IOException {
        return this.lockAndDoSelect(0L);
    }

    private int lockAndDoSelect(long var1) throws IOException {
        synchronized(this) {
            if(!this.isOpen()) {//检查这个Selector是否打开
                throw new ClosedSelectorException();
            } else {
                //双重锁
                Set var4 = this.publicKeys;
                int var10000;
                synchronized(this.publicKeys) {
                    Set var5 = this.publicSelectedKeys;
                    synchronized(this.publicSelectedKeys) {
                        var10000 = this.doSelect(var1);//分析
                    }
                }

                return var10000;
            }
        }
    } 

上面首先检查了这个Selector是否打开,如果没有打开,则抛异常,否则利用双重锁定调用了doSelect(var1)这个方法。

KQueueSelectorImpl.java

    protected int doSelect(long var1) throws IOException {
        boolean var3 = false;
        if(this.closed) {
            throw new ClosedSelectorException();
        } else {
            this.processDeregisterQueue();

            int var7;
            try {
                this.begin();
                var7 = this.kqueueWrapper.poll(var1);
            } finally {
                this.end();
            }

            this.processDeregisterQueue();
            return this.updateSelectedKeys(var7);
        }
    } 

重点在于var7 = this.kqueueWrapper.poll(var1);
这行代码,不太懂这个里面干了些什么。先不管。

KQueueArrayWrapper.java

    int poll(long var1) {
        this.updateRegistrations();
        int var3 = this.kevent0(this.kq, this.keventArrayAddress, 128, var1);
        return var3;
    }

到这里就看完了selectNow()方法,下面将分析如果taskQueue不为空是调用select(boolean oldWakenUp)方法逻辑。

2、如果taskQueue为空,则调用select(oldWakenUp)方法

select(oldWakenUp)方法的代码如下:

    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();
                        selectCnt = 1;
                    }
                    break;
                }

                int selectedKeys = selector.select(timeoutMillis);
                selectCnt ++;

                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;
                }
                if (Thread.interrupted()) {
                    // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
                    // As this is most likely a bug in the handler of the user or it's client library we will
                    // also log it.
                    //
                    // See https://github.com/netty/netty/issues/2426
                    if (logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely because " +
                                "Thread.currentThread().interrupt() was called. Use " +
                                "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                    }
                    selectCnt = 1;
                    break;
                }

                long time = System.nanoTime();
                if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                    // timeoutMillis elapsed without anything selected.
                    selectCnt = 1;
                } else 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);

                    rebuildSelector();
                    selector = this.selector;

                    // Select again to populate selectedKeys.
                    selector.selectNow();
                    selectCnt = 1;
                    break;
                }

                currentTimeNanos = time;
            }

            if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Selector.select() returned prematurely {} times in a row.", selectCnt - 1);
                }
            }
        } catch (CancelledKeyException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector - JDK bug?", e);
            }
            // Harmless exception - log anyway
        }
    }

上面的代码很长,逻辑也比较复杂哈,但如下的简化代码就是该方法select与selectNow()这个方法的主要区别:前者selectNow()方法是通过调用selector.selectNow()立即返回不会阻塞当前线程,而在这个 select 方法中, 调用了 selector.select(timeoutMillis), 而这个调用是会阻塞住当前线程的, timeoutMillis 是阻塞的超时时间。

    private void select(boolean oldWakenUp) throws IOException {
        //...
         int selectedKeys = selector.select(timeoutMillis);
         //...       
    }

上面的简化代码只是想说明在netty中select与selectNow方法的区别,下面还是详细的分析下select方法。具体分解为如下的8点:

1、delayNanos(currentTimeNanos)

该方法的代码如下:

    protected long delayNanos(long currentTimeNanos) {
        ScheduledFutureTask delayedTask = delayedTaskQueue.peek();
        if (delayedTask == null) {
            return SCHEDULE_PURGE_INTERVAL;//TimeUnit.SECONDS.toNanos(1);即1s
        }

        return delayedTask.delayNanos(currentTimeNanos);
    }


    ScheduledFutureTask.java

    public long delayNanos(long currentTimeNanos) {
        return Math.max(0, deadlineNanos() - (currentTimeNanos - START_TIME));//START_TIME为任务的创建时间
    }  
    public long deadlineNanos() {
        return deadlineNanos;
    } 

该方法主要是计算延迟任务队列中第一个任务的到期执行时间(即最晚还能延迟执行的时间)。注意:每个SingleThreadEventExecutor都持有一个延迟执行任务的优先队列:final Queue> delayedTaskQueue = new PriorityQueue>()),在启动线程的时候会往队列中加入一个任务)。最终的结果:{创建该任务指定的延迟时间-(当前时间-delayedTask创建的时间)}。如果队列中没有任何任务,则默认返回1秒钟。

在前面我们介绍启动线程的startThread()方法(代码如下)时,就添加了一个定时任务,该任务所做的事情为:将定时任务队列中的已取消任务从队列中移除,该任务每间隔1秒执行1次 ,即这个任务的deadlineNanos = SCHEDULE_PURGE_INTERVAL = 1s。

 SingleThreadEventExecutor.java

     private void startThread() {
        if (STATE_UPDATER.get(this) == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                delayedTaskQueue.add(new ScheduledFutureTask(
                        this, delayedTaskQueue, Executors.callable(new PurgeTask(), null),
                        ScheduledFutureTask.deadlineNanos(SCHEDULE_PURGE_INTERVAL), -SCHEDULE_PURGE_INTERVAL));
                thread.start();
            }
        }
    }

继续往后面看

2、从如下的代码可以得到:如果延迟任务队列中第一个任务的最晚还能延迟执行的时间小于500000纳秒,且selectCnt == 0(selectCnt 用来记录selector.select方法的执行次数和标识是否执行过selector.selectNow()),则执行selector.selectNow()方法并立即返回。

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

3、如果大于500000纳秒,则执行selector.select(timeoutMillis),这个方法在博文Java NIO 之 Selector(第二部分selector.select())中有详细的介绍。简单来说:select(long timeout)在等待有通道被选择时至多会阻塞timeout毫秒。

selector.select()函数功能:选择一些I/O操作已经准备好的管道。每个管道对应着一个key。这个方法 是一个阻塞的选择操作。当至少有一个通道被选择时才返回。当这个方法被执行时,当前线程是允许被中断的。

而select(long timeout)和select()一样,除了最长会阻塞timeout毫秒(参数)。

4、selector.select(timeoutMillis)执行完毕之后,则开始执行下面的代码:

                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;
                }
                 if (Thread.interrupted()) {
                    // Thread was interrupted so reset selected keys and break so we not run into a busy loop.
                    // As this is most likely a bug in the handler of the user or it's client library we will
                    // also log it.
                    //
                    // See https://github.com/netty/netty/issues/2426
                    if (logger.isDebugEnabled()) {
                        logger.debug("Selector.select() returned prematurely because " +
                                "Thread.currentThread().interrupt() was called. Use " +
                                "NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
                    }
                    selectCnt = 1;
                    break;
                }  

从上面的代码可知:如果存在以下几种情况则退出for循环:

1)已经ready的selectionKey,2)selector被唤醒,3)任务taskQueue不为空,4)scheduledTaskQueue不为空。5)线程被中断

5、如果time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos,即说明selector.select(timeoutMillis)等待了timeoutMillis长的时间,将selectCnt置为1,便于下次循环时退出循环。如果等待时间小于timeoutMillis时间,则进行第6步和7步。

6、如果 selectCnt 没达到阈值SELECTOR_AUTO_REBUILD_THRESHOLD(默认512),则继续进行for循环。其中 currentTimeNanos 在select操作之后会重新赋值当前时间,如果selector.select(timeoutMillis)行为真的阻塞了timeoutMillis,第二次的timeoutMillis肯定等于0,此时selectCnt 为1,所以会直接退出for循环。

7、如果selectCnt达到阈值,则说明,执行了这一段for循环达512次还没有break,这是什么原因呢?这是java早期的nio bug会导致cpu 100%, 此时select(timeout)根本不会阻塞(如果阻塞了则不能达到阈值哈)而直接返回0, 在netty中判断是否发生了此bug的方式为:为在很短时间内(小于1秒)完成了多次(默认512)select(timeout),则发生了该bug,此时进行rebuildSelector来消除bug。即如果selectCnt 达到阈值SELECTOR_AUTO_REBUILD_THRESHOLD,则会进行重建。就如下面rebuildSelector()方法上的注释所言:重建的目的就是消灭epoll 100% cpu bug。

rebuildSelector()方法的代码如下,比较长,但主要干了如下2件事:

1、通过调用openSelector()方法创建一个新的selector。

2、将old selector的selectionKey执行cancel,并将oldSelector中的channel重新register到newSelector上。

具体细节这里不研究。

重建的思路基本都是这样:1)创建一个新的;2)把旧的有用的参数赋值到新的上面;3)摧毁(或抛弃)旧的。

    /**
     * Replaces the current {@link Selector} of this event loop with newly created {@link Selector}s to work
     * around the infamous epoll 100% CPU bug.
     */
    public void rebuildSelector() {
    //inEventLoop()方法判断启动线程与当前线程相同,相同表示已经启动,不同则添加一个任务到该NioEventLoop的线程中
        if (!inEventLoop()) {
            execute(new Runnable() {
                @Override
                public void run() {
                    rebuildSelector();//重建
                }
            });
            return;
        }

        final Selector oldSelector = selector;
        final Selector newSelector;

        if (oldSelector == null) {
            return;
        }

        try {
                //第一步:通过调用openSelector()创建新的selector。
            newSelector = 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 (;;) {
            try {
            //第二步:将oldSelector中的所有key cancel,并将key上的channel重新register在newSelector上。
                for (SelectionKey key: oldSelector.keys()) {
                    Object a = key.attachment();
                    try {
                        if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
                            continue;
                        }

                        int interestOps = key.interestOps();
                        key.cancel();//将old selector上的key cancel掉
                        //将old selector上的channel重新注册到new selector上
                        SelectionKey newKey = key.channel().register(newSelector, 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 task = (NioTask) a;
                            invokeChannelUnregistered(task, key, e);
                        }
                    }
                }
            } catch (ConcurrentModificationException e) {
                // Probably due to concurrent modification of the key set.
                continue;
            }

            break;
        }

        selector = newSelector;

        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.");
    }

8、在重建完成之后,再一次调用方法selectNow,检查是否有已ready的selectionKey,为后面的处理做准备。

3、run方法的后半部分的分析

到这里就分析了NioEventLoop类中run方法的前面一部分:通过 select/selectNow 调用查询当前是否有就绪的 IO 事件。下面继续run方法的后半部分(如下):如果前面一部分通过select/selectNow查询有 IO 事件就绪时, 这一步就是处理这些 IO 事件了。

                if (ioRatio == 100) {
                    processSelectedKeys();
                    runAllTasks();
                } else {
                    final long ioStartTime = System.nanoTime();

                    processSelectedKeys();

                    final long ioTime = System.nanoTime() - ioStartTime;
                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }

前面说过:在 Netty 中, 一个 EventLoop 有两个职责, 第一个是作为 IO 线程, 负责相应的 IO 操作; 第二个是作为任务线程, 执行 taskQueue 中的任务。

每个职责对应的一个处理函数,在上面的代码中反应了出来,第一个是 processSelectedKeys()函数负责处理第一步select/selectNow查询所得到的就绪的 IO 事件,第二个是 runAllTasks()函数负责处理taskQueue中的任务。

既然这个EventLoop所对应的Java 本地线程需要处理这两种任务,因此就会有一定的时间分配哈,是吧,因此代码中的 ioRatio 变量表示的就是此线程分配给 IO 操作所占的时间比(即 运行processSelectedKeys 耗时在整个循环中所占用的时间),例如:ioRatio默认为 50 ,表示的就是 执行 IO 操作和执行 tash 任务 所占的时间比值为1:1。

当知道了 IO 操作耗时和它所占用的时间比, 那么执行 task 的时间就可以很方便的计算出来了:

设 IO 操作耗时为 ioTime, ioTime 占的时间比例为 ioRatio, 则:
    ioTime / ioRatio = taskTime / taskRatio
    taskRatio = 100 - ioRatio
    => taskTime = ioTime * (100 - ioRatio) / ioRatio

当 ioRatio 为 100 时, Netty 就不考虑 IO 耗时的占比, 而是分别调用 processSelectedKeys()、runAllTasks(); 而当 ioRatio 不为 100时, 则执行到 else 分支, 在这个分支中, 首先记录下 processSelectedKeys() 所执行的时间(即 IO 操作的耗时), 然后根据公式, 计算出执行 task 所占用的时间, 然后以此为参数, 调用 runAllTasks()。

以上就是上面这块代码的核心思想,下面将具体来分析下用于IO操作的processSelectedKeys() 函数,然后分析用于 处理 task任务的 runAllTasks() 函数。

1、processSelectedKeys() 分析

此方法的源码如下:

    private void processSelectedKeys() {
        if (selectedKeys != null) {
            processSelectedKeysOptimized(selectedKeys.flip());
        } else {
            processSelectedKeysPlain(selector.selectedKeys());
        }
    }

先看processSelectedKeysOptimized方法,processSelectedKeysPlain方法与该方法类似,本博文就不分析此方法了。

    private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) {
        for (int i = 0;; i ++) {
            final SelectionKey k = selectedKeys[i];
            // 在获取所有key(即flip)时会将最后一个有效key的下一个位置值为null,因此碰到null,说明所有有效的key已经获取完 ,因此利用break跳出循环
            if (k == null) {
                break;
            }
            // null out entry in the array to allow to have it GC'ed once the Channel close
            // See https://github.com/netty/netty/issues/2363
            selectedKeys[i] = null;//方便gC

            final Object a = k.attachment();
            // key关联两种不同类型的对象,一种是AbstractNioChannel,一种是NioTask 
            if (a instanceof AbstractNioChannel) {
                processSelectedKey(k, (AbstractNioChannel) a);
            } else {
                @SuppressWarnings("unchecked")
                NioTask task = (NioTask) a;
                processSelectedKey(k, task);
            }
            // 如果需要重新select则重置当前数据 
            if (needsToSelectAgain) {
                // null out entries in the array to allow to have it GC'ed once the Channel close
                // See https://github.com/netty/netty/issues/2363
                for (;;) {
                    if (selectedKeys[i] == null) {
                        break;
                    }
                    selectedKeys[i] = null;
                    i++;
                }

                selectAgain();
                // Need to flip the optimized selectedKeys to get the right reference to the array
                // and reset the index to -1 which will then set to 0 on the for loop
                // to start over again.
                //
                // See https://github.com/netty/netty/issues/1523
                selectedKeys = this.selectedKeys.flip();
                i = -1;
            }
        }
    } 

此方法的代码也比较长哈,但是关键的点就两个: 迭代 selectedKeys 获取就绪的 IO 事件, 然后为每个事件都调用 processSelectedKey 来处理它.

上面的处理过程中有一个needsToSelectAgain,什么情况下会触发这个条件呢。当多个channel从selector中撤销注册时,由于很多数据无效了(默认为256),需要重新处理,设置needsToSelectAgain=true的函数如下:

     void cancel(SelectionKey key) {
        key.cancel();
        cancelledKeys ++;
        if (cancelledKeys >= CLEANUP_INTERVAL) {
            cancelledKeys = 0;
            needsToSelectAgain = true;
        }
    } 

接下来看下processSelectedKey

     private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
        final NioUnsafe unsafe = ch.unsafe();
        //检查该SelectionKey是否有效,如果无效,则关闭channel
        if (!k.isValid()) {
            // close the channel if the key is not valid anymore
            unsafe.close(unsafe.voidPromise());
            return;
        }

        try {
            int readyOps = k.readyOps();
            // Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
            // to a spin loop
            // 如果准备好READ或ACCEPT则触发unsafe.read() ,检查是否为0,如上面的源码英文注释所说:解决JDK可能会产生死循环的一个bug。
            if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
                unsafe.read();
                if (!ch.isOpen()) {//如果已经关闭,则直接返回即可,不需要再处理该channel的其他事件
                    // Connection already closed - no need to handle write.
                    return;
                }
            }
            // 如果准备好了WRITE则将缓冲区中的数据发送出去,如果缓冲区中数据都发送完成,则清除之前关注的OP_WRITE标记
            if ((readyOps & SelectionKey.OP_WRITE) != 0) {
                // Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
                ch.unsafe().forceFlush();
            }
            // 如果是OP_CONNECT,则需要移除OP_CONNECT否则Selector.select(timeout)将立即返回不会有任何阻塞,这样可能会出现cpu 100%
            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();
            }
        } catch (CancelledKeyException ignored) {
            unsafe.close(unsafe.voidPromise());
        }
    } 

该函数主要SelectionKey k进行了检查,然后如下几种不同的情况

1)OP_READ, 可读事件, 即 Channel 中收到了新数据可供上层读取。

2)OP_WRITE, 可写事件, 即上层可以向 Channel 写入数据。

3)OP_CONNECT, 连接建立事件, 即 TCP 连接已经建立, Channel 处于 active 状态.

2、runAllTasks()分析

最后分析下NioEventLoop类run方法中的最后调用的一个方法:runAllTasks();

    //SingleThreadEventExecutor.java

    /**
     * Poll all tasks from the task queue and run them via {@link Runnable#run()} method.
     *
     * @return {@code true} if and only if at least one task was run
     */
    protected boolean runAllTasks() {
        fetchFromDelayedQueue();
        Runnable task = pollTask();
        if (task == null) {
            return false;
        }

        for (;;) {
            try {
                task.run();
            } catch (Throwable t) {
                logger.warn("A task raised an exception.", t);
            }

            task = pollTask();
            if (task == null) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                return true;
            }
        }
    }

该函数的功能:从任务队列中拉取所有的任务,然后运行这些任务的run方法来执行他们。如果至少有一个任务被执行就返回true。具体思路如下2点:

1)、通过调用如下的fetchFromDelayedQueue函数从定时任务队列拉取已经到“执行时间”的任务到任务队列;例如:你有一个任务9点中开始,最晚执行时间为10点,如果现在时间为10点半,则此任务就会从延迟任务队列delayedTaskQueue加入到任务队列taskQueue中去。

    private void fetchFromDelayedQueue() {
        long nanoTime = 0L;
        for (;;) {
            ScheduledFutureTask delayedTask = delayedTaskQueue.peek();
            if (delayedTask == null) {
                break;
            }

            if (nanoTime == 0L) {
                nanoTime = ScheduledFutureTask.nanoTime();//System.nanoTime() - START_TIME;
            }

            if (delayedTask.deadlineNanos() <= nanoTime) {
                delayedTaskQueue.remove();
                taskQueue.add(delayedTask);
            } else {
                break;
            }
        }
    }

2)、通过调用如下的pollTash()方法获取任务队列中任务,然后调用该任务的run方法执行此任务,就这样依次执行完任务队列中所有的任务。

    protected Runnable pollTask() {
        assert inEventLoop();
        for (;;) {
            Runnable task = taskQueue.poll();
            if (task == WAKEUP_TASK) {
                continue;
            }
            return task;
        }
    }

runAllTasks()方法的另一个重载的方法的代码如下所示,runAllTasks(long timeoutNanos)该方法与runAllTasks()的不同在于:有执行时间的限制,即只允许执行一定的时间,时间到即使任务还没有全部执行完则退出。

    /**
     * Poll all tasks from the task queue and run them via {@link Runnable#run()} method.  This method stops running
     * the tasks in the task queue and returns if it ran longer than {@code timeoutNanos}.
     */
    protected boolean runAllTasks(long timeoutNanos) {
        fetchFromDelayedQueue();
        Runnable task = pollTask();
        if (task == null) {
            return false;
        }

        final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
        long runTasks = 0;
        long lastExecutionTime;
        for (;;) {
            try {
                task.run();
            } catch (Throwable t) {
                logger.warn("A task raised an exception.", t);
            }

            runTasks ++;

            // 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;
                }
            }

            task = pollTask();
            if (task == null) {
                lastExecutionTime = ScheduledFutureTask.nanoTime();
                break;
            }
        }

        this.lastExecutionTime = lastExecutionTime;
        return true;
    } 

到这里,我们就从以NioEventLoop类execute方法作为切入点,研究了EventLoop所对应的Java 本地线程的启动,研究了此线程启动后是如何来处理EventLoop所负责的IO操作和task任务的。

参考资料

1、https://segmentfault.com/a/1190000007403937

2、http://blog.csdn.net/youaremoon/article/details/50311341

3、http://xw-z1985.iteye.com/blog/1928244

你可能感兴趣的:(netty源码分析,源码,Netty,EventLoop)