Netty源码篇10-核心组件EventLoop源码分析

834 阅读7分钟

欢迎大家关注 github.com/hsfxuebao ,希望对大家有所帮助,要是觉得可以的话麻烦给点一下Star哈

1. EventLoop 介绍

继承图:

image.png

说明:

  • ScheduledExecutorService 接口表示是一个定时任务接口,EventLoop 可以接受定时任务。

  • EventLoop 接口:Netty 接口文档说明该接口作用:一旦 Channel 注册了,就处理该 Channel 对应的所有I/O 操作。

  • SingleThreadEventExecutor 表示这是一个单个线程的线程池

  • EventLoop 是一个单例的线程池,里面含有一个死循环的线程不断的做着 3 件事情:监听端口,处理端口事件,处理队列事件。每个 EventLoop 都可以绑定多个 Channel,而每个 Channel 始终只能由一个 EventLoop 来处理

2. NioEventLoop 用的使用 - execute 方法

2.1 源码剖析

image.png

在 EventLoop 的 使 用 , 一 般 就 是 eventloop.execute(task)。看 下 execute 方 法 的 实 现 ( 在SingleThreadEventExecutor 类中)

@Override
public void execute(Runnable task) {
    ObjectUtil.checkNotNull(task, "task");
    execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));
}
private void execute(Runnable task, boolean immediate) {
    /**
     * 判断Thread.currentThread()当前线程是不是与NioEventLoop绑定的本地线程;
     * 如果Thread.currentThread()== this.thread, 那么只用将execute()方法中的task添加到任务队列中就好;
     * 如果Thread.currentThread()== this.thread 返回false, 那就先调用startThread()方法启动本地线程,然后再将task添加到任务队列中.
     */
    boolean inEventLoop = inEventLoop();
    addTask(task);
    if (!inEventLoop) {
        startThread(); // 启动线程
        if (isShutdown()) {
            boolean reject = false;
            try {
                if (removeTask(task)) {
                    reject = true;
                }
            } catch (UnsupportedOperationException e) {
              ...
            }
            if (reject) {
                reject();
            }
        }
    }
    if (!addTaskWakesUp && immediate) {
        wakeup(inEventLoop);
    }
}

@Override
public void execute(Runnable task) {
    if (task == null) {
        throw new NullPointerException("task");
    }
    boolean inEventLoop = inEventLoop();
    if (inEventLoop) {
        addTask(task);
    } else {
        startThread();
        addTask(task);
        if (isShutdown() && removeTask(task)) {
            reject();
        }
    }
    if (!addTaskWakesUp && wakesUpForTask(task)) {
        wakeup(inEventLoop);
    }
}

说明:

  • 首先判断该 EventLoop 的线程是否是当前线程,然后直接添加到任务队列中

    • 如果不是,则尝试启动线程(但由于线程是单个的,因此只能启动一次)。
  • 如果线程已经停止,并且删除任务失败,则执行拒绝策略,默认是抛出异常。

  • 如果 addTaskWakesUp 是 false,并且任务不是 NonWakeupRunnable 类型的,就尝试唤醒 selector。这个时候,阻塞在 selecor 的线程就会立即返回

2.1.1 addTask 和 offerTask 方法源码

//addTask
protected void addTask(Runnable task) {
    ObjectUtil.checkNotNull(task, "task");
    if (!offerTask(task)) {
        reject(task);
    }
}

//offerTask
final boolean offerTask(Runnable task) {
    if (isShutdown()) {
        reject();
    }
    return taskQueue.offer(task);
}

2.1.2 NioEventLoop 类的父类 SingleThreadEventExecutor的 startThread 方法

当执行 execute 方法的时候,如果当前线程不是 EventLoop 所属线程,则尝试启动线程,也就是 startThread 方法,dubug 代码如下

private void startThread() {
    if (state == ST_NOT_STARTED) {
        //设置thread 的状态 this.state是 ST_STARTED 已启动
        if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
            boolean success = false;
            try {
                //真正启动线程的函数, 其作用类似于 thread.start()
                doStartThread();
                success = true;
            } finally {
                if (!success) {
                    STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
                }
            }
        }
    }
}

说明:

  • 该方法首先判断是否启动过了,保证 EventLoop 只有一个线程,如果没有启动过,则尝试使用 Cas 将 state 状态改为 ST_STARTED,也就是已启动。然后调用 doStartThread 方法。如果失败,则进行回滚。
private void doStartThread() {
    //启动线程之前,必须保证thread 是null,其实也就是thread还没有启动。
    assert thread == null;
    /** 通过executor启动一个新的task, 在task里面启动this.thread线程。*/
    executor.execute(new Runnable() {
        @Override
        public void run() {
            // 1. 将当前thread 赋值给 this.thread 也就是将启动的线程赋值给本地绑定线程thread
            thread = Thread.currentThread();
            if (interrupted) {
                thread.interrupt();
            }

            boolean success = false;
            updateLastExecutionTime();
            try {
                // 2. 实际上是调用NioEventLoop.run() 方法实现 事件循环机制
                SingleThreadEventExecutor.this.run();
                success = true;
            } catch (Throwable t) {
               ...
            } finally {
                for (;;) { // 死循环
                    int oldState = state;
                    if (oldState >= ST_SHUTTING_DOWN || STATE_UPDATER.compareAndSet(
                            SingleThreadEventExecutor.this, oldState, ST_SHUTTING_DOWN)) {
                        break;
                    }
                }
                ...
                try {
                    for (;;) {
                        if (confirmShutdown()) {
                            break;
                        }
                    }

                    for (;;) {
                        int oldState = state;
                        if (oldState >= ST_SHUTDOWN || STATE_UPDATER.compareAndSet(
                                SingleThreadEventExecutor.this, oldState, ST_SHUTDOWN)) {
                            break;
                        }
                    }
                    confirmShutdown();
                } finally {
                    //确保事件循环结束之后,关闭线程,清理资源。
                    try {
                        cleanup();
                    } finally {
                        
                        FastThreadLocal.removeAll();

                        STATE_UPDATER.set(SingleThreadEventExecutor.this, ST_TERMINATED);
                        threadLock.countDown();
                        int numUserTasks = drainTasks();
                        if (numUserTasks > 0 && logger.isWarnEnabled()) {
                            logger.warn("An event executor terminated with " +
                                    "non-empty task queue (" + numUserTasks + ')');
                        }
                        terminationFuture.setSuccess(null);
                    }
                }
            }
        }
    });
}

说明:

  • 首先调用 executor 的 execute 方法,这个 executor 就是在创建 Event LoopGroup 的时候创建的ThreadPerTaskExecutor 类。该 execute 方法会将 Runnable 包装成 Netty 的 FastThreadLocalThread。

  • 任务中,首先判断线程中断状态,然后设置最后一次的执行时间。

  • 执行当前 NioEventLoop 的 run 方法,注意:这个方法是个死循环,是整个 EventLoop 的核心

  • 在 finally 块中,使用 CAS 不断修改 state 状态,改成 ST_SHUTTING_DOWN。也就是当线程 Loop 结束的时候。关闭线程。最后还要死循环确认是否关闭,否则不会 break。然后,执行 cleanup 操作,更新状态为ST_TERMINATED,并释放当前线程锁。如果任务队列不是空,则打印队列中还有多少个未完成的任务。并回调 terminationFuture 方法。

  • 其实 最核心的就是 Event Loop 自身的 run 方法。再继续深入 run 方法

2.2 EventLoop 的中的 Loop 是靠 run 实现的, run 方法(在该方法在 NioEventLoop)

protected void run() {
    int selectCnt = 0;
    //所有的逻辑操作都在for循环体内进行,只有当NioEventLoop接收到退出指令的时候,才退出循环,否则一直执行下去,这也是通用的NIO线程实现方式。
    /** 死循环:NioEventLoop 事件循环的核心就是这里! */
    for (;;) {
        try {
            int strategy;
            try {
                // 1.通过 select/selectNow 调用查询当前是否有就绪的 IO 事件
                // 当 selectStrategy.calculateStrategy() 返回的是 CONTINUE, 就结束此轮循环,进入下一轮循环;
                // 当返回的是 SELECT, 就表示任务队列为空,就调用select(Boolean);
                strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
                switch (strategy) {
                case SelectStrategy.CONTINUE:
                    continue;

                case SelectStrategy.BUSY_WAIT:
                    // fall-through to SELECT since the busy-wait is not supported with NIO

                case SelectStrategy.SELECT:
                    long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
                    if (curDeadlineNanos == -1L) {
                        curDeadlineNanos = NONE; // nothing on the calendar
                    }
                    nextWakeupNanos.set(curDeadlineNanos);
                    try {
                        if (!hasTasks()) {
                            strategy = select(curDeadlineNanos);
                        }
                    } finally {
                        // This update is just to help block unnecessary selector wakeups
                        // so use of lazySet is ok (no race condition)
                        nextWakeupNanos.lazySet(AWAKE);
                    }
                    // fall through
                default:
                }
            } catch (IOException e) {
                // If we receive an IOException here its because the Selector is messed up. Let's rebuild
                // the selector and retry. https://github.com/netty/netty/issues/8566
                rebuildSelector0();
                selectCnt = 0;
                handleLoopException(e);
                continue;
            }

            //2. 当有IO事件就绪时, 就会处理这些IO事件
            selectCnt++;
            cancelledKeys = 0;
            needsToSelectAgain = false;
            //ioRatio表示:此线程分配给IO操作所占的时间比(即运行processSelectedKeys耗时在整个循环中所占用的时间).
            final int ioRatio = this.ioRatio;
            boolean ranTasks;
            if (ioRatio == 100) {
                try {
                    if (strategy > 0) {
                        //查询就绪的 IO 事件, 然后处理它;
                        processSelectedKeys();
                    }
                } finally {
                    // Ensure we always run tasks.
                    //运行 taskQueue 中的任务.
                    ranTasks = runAllTasks();
                }
            } else if (strategy > 0) {
                final long ioStartTime = System.nanoTime();
                try {
                    //查询就绪的 IO 事件, 然后处理它;
                    processSelectedKeys();
                } finally {
                    // Ensure we always run tasks.
                    final long ioTime = System.nanoTime() - ioStartTime;
                    ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
                }
            } else {
                ranTasks = runAllTasks(0); // This will run the minimum number of tasks
            }

            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)
                selectCnt = 0;
            }
        } catch (CancelledKeyException e) {
            // Harmless exception - log anyway
            if (logger.isDebugEnabled()) {
                logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
                        selector, e);
            }
        } catch (Error e) {
            throw e;
        } catch (Throwable t) {
            handleLoopException(t);
        } finally {
            // Always handle shutdown even if the loop processing threw an exception.
            try {
                if (isShuttingDown()) {
                    closeAll();
                    if (confirmShutdown()) {
                        return;
                    }
                }
            } catch (Error e) {
                throw e;
            } catch (Throwable t) {
                handleLoopException(t);
            }
        }
    }
}

说明:

从上面的步骤可以看出,整个 run 方法做了 3 件事情:

  • select 获取感兴趣的事件。
  • processSelectedKeys 处理事件。
  • runAllTasks 执行队列中的任务

上面的三个方法,select 方法(体现非阻塞)

核心 select 方法解析:

private int select(long deadlineNanos) throws IOException {
    if (deadlineNanos == NONE) {
        return selector.select();
    }
    // Timeout will only be 0 if deadline is within 5 microsecs
    long timeoutMillis = deadlineToDelayNanos(deadlineNanos + 995000L) / 1000000L;
    return timeoutMillis <= 0 ? selector.selectNow() : selector.select(timeoutMillis);
}

说明:

  • 调用 selector 的 select 方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上在加上 0.5秒进行阻塞。当执行 execute 方法的时候,也就是添加任务的时候,唤醒 selecor,防止 selecotr 阻塞时间过长

3. EventLoop 核心的运行机制小结

  • 每次执行 ececute 方法都是向队列中添加任务。

  • 当第一次添加时就启动线程,执行 run 方法,而 run 方法是整个 EventLoop 的核心,就像 EventLoop 的名字一样,Loop Loop ,不停的 Loop ,Loop 做什么呢?做 3 件事情。

  • 调用 selector 的 select 方法,默认阻塞一秒钟,如果有定时任务,则在定时任务剩余时间的基础上在加上 0.5秒进行阻塞。当执行 execute 方法的时候,也就是添加任务的时候,唤醒 selecor,防止 selecotr 阻塞时间过长。

  • 当 selector 返回的时候,回调用 processSelectedKeys 方法对 selectKey 进行处理。

  • 当 processSelectedKeys 方法执行结束后,则按照 ioRatio 的比例执行 runAllTasks 方法,默认是 IO 任务时间和非 IO 任务时间是相同的,你也可以根据你的应用特点进行调优 。比如 非 IO 任务比较多,那么你就将ioRatio 调小一点,这样非 IO 任务就能执行的长一点。防止队列积攒过多的任务。

参考文档

Netty学习和源码分析github地址
Netty从入门到精通视频教程(B站)
Netty权威指南 第二版