Netty源码分析——Reactor的select

1,691 阅读5分钟

Netty源码分析——Reactor的select

Netty使用的模式,实际就是这个Reactor模式,Reactor线程也是最重要的线程,承载着Netty中的读操作,然后也是整个Pipeline的触发点。

映射到Netty里,用的最多的就是NioEventLoop,其他还有EpollEventLoopKQueueEventLoop等等。这篇文章只关注NioEventLoop

启动

启动是在添加任务的时候启动的,入口在SingleThreadEventExecutor#execute中:

boolean inEventLoop = inEventLoop();
addTask(task);
if (!inEventLoop) {
startThread();
if (isShutdown() && removeTask(task)) {
reject();
}
}

先添加任务,外部线程在往任务队列里面添加任务的时候执行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);
}
}
}

netty会判断reactor线程有没有被启动。继续看doStartThread

executor.execute(new Runnable() {
@Override
public void run() {
//sth...

boolean success = false;
try {
//这里的run实际上是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;
}
}
} finally {
//sth...
}
}
}
});

这里主要是把NioEventLooprun包装了一下,放线程池里去执行,执行之后,如果中断了或者结束了,把剩下的任务和钩子都跑完,当然这之前要进行整个EventLoop的状态流转。

默认使用线程池是ThreadPerTaskExecutor,在每次执行execute方法的时候都会通过DefaultThreadFactory创建一个FastThreadLocalThread线程。

这个线程就是我们的Reactor线程。这里简单提一句,Netty中的线程和ThreadLocal都已经优化过了。

run起来!

主要说说run方法,主要来说就做三件事,select处理select到的key处理提交过来的任务

这里要盗一张图了,因为这个图做的实在太棒了:

一个EventLoop中,可以注册多个Channel(图作者连这个都画上去了,非常厉害),进行select操作。

EventLoop一直在不停的处理这三件事情:

  1. 首先轮询注册到Reactor线程对应的selector上的所有Channel的IO事件。(一个EventLoop会包含一个selectorNioEventLoop对应的selector就是原生java nio的selector
  2. 处理产生网络IO事件的Channel
  3. 处理任务队列。

顺序看一下三件事。第一件事:

select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}

wakenUp表示是否应该唤醒正在阻塞的select操作,可以看到netty在进行一次新的loop之前,都会将wakeUp被设置成false,标志新的一轮loop的开始。

看下这个select方法,拆分一下来看:

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

//sth...
}

先看下这个selectDeadLineNanos是什么,字面意思是select操作的截止时间,看下这个delayNanos

ScheduledFutureTask<?> scheduledTask = peekScheduledTask();
if (scheduledTask == null) {
return SCHEDULE_PURGE_INTERVAL;
}
return scheduledTask.delayNanos(currentTimeNanos);

这里实际上是取scheduledTaskQueue里的头上的任务(是按照延迟时间从小到大进行排序),并且计算截止时间。delayNanos方法注释也说明了:返回执行最接近deadline的计划任务之前剩余的时间量。也就是最近要快结束的定时任务的时间。

综合一下,for循环里第一个部分的代码作用就是:如果发现当前的定时任务队列中有任务的截止事件快到了(剩余时间<=0.5ms),就跳出循环。此外,跳出之前如果发现目前为止还没有进行过select操作(selectCnt == 0),那么就调用一次selectNow(),该方法会立即返回,不会阻塞。

继续看select方法的下个部分:

if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}

这个部分也有很长一段注释,意思是说,如果有任务被提交进来,防止这个任务一直不被执行(下面就是select阻塞,可能一直不返回,则这个任务一直不会被执行),这里会进行一次非阻塞的select并且跳出去执行任务。

继续往下看select,这个部分是阻塞的select,会一直阻塞一段时间:

int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}

在这里进行一次阻塞select操作,截止到第一个定时任务的截止时间。如果这里外部线程提交任务,则不会一直阻塞timeoutMillis时长,而是直接被唤醒,这也是防止在select之后,有新的需要立即执行的任务被提交进来,由于timeoutMillis可能很长,导致新任务一直不被执行,具体的代码在SingleThreadEventExecutor#execute中:

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

protected void wakeup(boolean inEventLoop) {
//如果是外部线程提交的任务才会唤醒select操作
if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
selector.wakeup();
}
}

注意的是,如果提交的任务是NonWakeupRunnable则不会唤醒select操作。

select操作看完了,其实主旨就是,有任务进来就先去执行任务,或者select到最早要到期的定时任务开始时间。再综合一下上面的图,我们就大概能理解NioEventLoop包装过的select操作了。

这里Netty还解决了臭名昭著的Select空轮训问题:

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.
rebuildSelector();
selector = this.selector;
// Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
}

netty会在每次进行阻塞轮训(selector.select(timeoutMillis))之前记录一下开始时间currentTimeNanos,在select之后记录一下结束时间,判断select操作是否至少持续了timeoutMillis秒,如果持续的时间大于等于timeoutMillis,说明就是一次有效的轮询,重置selectCnt标志。

否则,表明该阻塞方法并没有阻塞这么长时间,可能触发了jdk的空轮询bug,当空轮询的次数超过一个阀值的时候(默认512),就开始执行rebuildSelector,代码里的注释:Rebuild the selector to work around the problem。需要注意的是,并非只要出现一次select时间不足timeoutMillis就认为触发了bug,因为selector.select(timeoutMillis)的返回条件之一是如果有一个channel被selected。

至此select操作我们就看完了。关于rebuildSelector这里不展开说了,主要是用新的selector替换老的selector,然后废弃老的selector即可。