Netty源码分析——Reactor的select
Netty使用的模式,实际就是这个Reactor模式
,Reactor线程也是最重要的线程,承载着Netty中的读操作,然后也是整个Pipeline
的触发点。
映射到Netty里,用的最多的就是NioEventLoop
,其他还有EpollEventLoop
、KQueueEventLoop
等等。这篇文章只关注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...
}
}
}
});
这里主要是把NioEventLoop
的run
包装了一下,放线程池里去执行,执行之后,如果中断了或者结束了,把剩下的任务和钩子都跑完,当然这之前要进行整个EventLoop的状态流转。
默认使用线程池是ThreadPerTaskExecutor
,在每次执行execute方法的时候都会通过DefaultThreadFactory
创建一个FastThreadLocalThread
线程。
这个线程就是我们的Reactor线程。这里简单提一句,Netty中的线程和ThreadLocal
都已经优化过了。
run起来!
主要说说run方法,主要来说就做三件事,select
、处理select到的key
和处理提交过来的任务
。
这里要盗一张图了,因为这个图做的实在太棒了:
一个EventLoop
中,可以注册多个Channel
(图作者连这个都画上去了,非常厉害),进行select
操作。
EventLoop
一直在不停的处理这三件事情:
- 首先轮询注册到
Reactor
线程对应的selector
上的所有Channel
的IO事件。(一个EventLoop
会包含一个selector
,NioEventLoop
对应的selector
就是原生java nio的selector
) - 处理产生网络IO事件的
Channel
。 - 处理任务队列。
顺序看一下三件事。第一件事:
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即可。