NioEventLoop创建、启动、执行核心要点分析

454 阅读3分钟

创建

NioEventLoop的创建主要做了如下几件事情:

  1. 创建TheadPerTaskExecutor, TPTE的作用就是根据线程工厂创建新的线程。
  2. 创建一组EventExecutor,也就是EventLoop。
  3. EventLoop初始化,创建两个队列tailTasks、taskQueue。taskQueue用来执行任务,tailTasks用来进行收尾工作。
  4. 创建优化后的多路复用器。JDK的多路复用器存储SelectionKey用的是HashSet,优化后的使用数组。
  5. 创建EventLoop选择器,用于轮训选择EventLoop。

启动

NioEventLoop的启动时机有两个:

  • 服务端bind
  • 新连接接入

以服务端绑定为例

启动

  1. 创建channel。
  2. next()是通过线程选择器选择一个NioEventLoop。
  3. 将NioEventLoop和这个channle绑定起来,下次这个channel直接使用这个将NioEventLoop。
  4. 执行NioEventLoop的execute方法,提交注册任务
  5. 判读这个线程是不是EventLoop线程
  6. 将任务添加到taskQueue
  7. 启动线程
  8. CAS判断NioEventLoop的状态
  9. 使用NioEventLoop的ThreadPerTaskExecutor执行任务
  10. 将ThreadPerTaskExecutor创建的当前线程赋值NioEventLoop的thread属性
  11. 执行run方法

EventLoop实现串行执行的关键

这里有几个非常关键的点也是NioEventLoop实现串行化执行的关键点。

EventLoop定义了一个state属性,分别有以下几个状态

private static final int ST_NOT_STARTED = 1;
private static final int ST_STARTED = 2;
private static final int ST_SHUTTING_DOWN = 3;
private static final int ST_SHUTDOWN = 4;
private static final int ST_TERMINATED = 5;

在启动Eventloop的时候借助了JDK的AtomicIntegerFiledUpdater,原子化更新state,无锁化保证在高并发情况下,EventLoop只启动一次。启动的时候会把当前的线程赋值到thread属性。

private static final AtomicIntegerFieldUpdater<SingleThreadEventExecutor> STATE_UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(SingleThreadEventExecutor.class, "state");

private void startThread() {
        if (state == ST_NOT_STARTED) {
            if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
                try {
                    doStartThread();
                } catch (Throwable cause) {
                    STATE_UPDATER.set(this, ST_NOT_STARTED);
                    PlatformDependent.throwException(cause);
                }
            }
        }
    }

private void doStartThread() {
        assert thread == null;
        executor.execute(new Runnable() {
            @Override
            public void run() {
                // 赋值
                thread = Thread.currentThread();
                if (interrupted) {
                    thread.interrupt();
                }
            }
       }
    }

所以使用EventLoop的时候,会先用isEventLoop判断当前线程是不是EventLoop线程,是的话直接执行,不是的话执行启动流程。

所以标准使用EventLoop的写法是:

@Test
public void event_loop_test() {
    DefaultEventLoop eventLoop = new DefaultEventLoop();
    boolean inEventLoop = eventLoop.inEventLoop();
    if (inEventLoop) {
            doSomething();
    } else {
        eventLoop.execute(() -> {
                doSomething();
        });
    }
}

执行

NioEventLoop的run方法主要做了三件事情:

  1. 调用 select()从操作系统中轮询到网络 IO 事件。
  2. 处理 IO 事件。
  3. 处理 nioEventLoop 的任务队列中的普通任务和定时任务。

因为NioEventLoop即需要处理IO事件又需要处理普通任务和定时任务,所以NioEventLoop优先处理taskQueue中的任务。因为NioEventLoop只有一个线程处理任务,如果普通任务耗时比较多,将会阻塞,导致IO事件无法被处理。

通过ioRatio可以调整IO时间和普通任务时间的占比。

如何规避JDK的空轮训BUG

for (; ; ) {
    // ...
    阻塞执行轮训
    int selectedKeys = selector.select(timeoutMillis);
    selectCnt ++;
    // 省略其他和空轮询无关的代码
	long time = System.nanoTime();
    // 下面这一行代码等价于: time - currentTimeNanos >= TimeUnit.MILLISECONDS.toNanos(timeoutMillis)
    // 什么意思呢?当前时间 - 运行之前的时间,如果大于 select阻塞的时间,这就说明进行了select的阻塞操作,没有进行JDK的空轮询
    if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
        selectCnt = 1;
    } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
            selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
        // 如果selectCnt的值大于等于512(这个数值可配置),说明系统可能进行了空轮训,因此创建新的selector,并用新的selector来替换之前的selector
        selector = selectRebuildSelector(selectCnt);
        selectCnt = 1;
        break;
    }
    currentTimeNanos = time;
}

首先Netty会计算多路复用器调用select的时间,每调用一次多路复用器的select,selectCnt的值就加1。

如果(当前时间>=阻塞时间+运行时间)证明没有发生空轮训,selectCnt设置为1。如果发生了空轮训,select没有阻塞,就不会(当前时间>=阻塞时间+运行时间),那么就创建一个新的多路复用器,替换当前的掉当前的多路复用器。

EventLoop选择器设计

选择器