在Netty启动后,Netty的线程池会起一个Selector线程处理IO事件和其他业务事件,下面来看下Selector流程
流程图
Selector线程是一个循环线程它一直处理IO事件和其他业务事件。这里需要说明Selector线程是处理IO事件和处理其他业务共享线程,也就是说Selector线程会按用户配置比例来处理IO接事件和其他业务事件(如channel注册事件),可能这次在执行IO事件下次如果有其他任务来了就忙其他任务去了。
源码和实现分析
Selector的主流程就是一个run()方法,源码如下:
@Override
protected void run() {
for (;;) {
//wakenUp设置成false,并获取原来wakenUp状态
boolean oldWakenUp = wakenUp.getAndSet(false);
try {
//判断当前任务队列是否有任务,如果有任务直接快速select一次以便能处理到任务
if (hasTasks()) {
selectNow();
} else {
//如果任务队列没有任务则进行阻塞select并让出cup时间片
select(oldWakenUp);
//如果wakenUp是true,则中断select一次
if (wakenUp.get()) {
selector.wakeup();
}
}
//处理io事件和根据占时比例配置处理任务
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
//处理IO事件
processSelectedKeys();
//处理任务
runAllTasks();
} else {
final long ioStartTime = System.nanoTime();
//处理IO事件
processSelectedKeys();
final long ioTime = System.nanoTime() - ioStartTime;
//处理任务
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
break;
}
}
} catch (Throwable t) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
}
}
}
主要有以下几个流程
- 循环开始先把唤醒标志wakenUp设置成false,并获取原来wakenUp状态。
这里的wakenUp是控制select的中断的标志,如果有其他任务加入会中断线程的select,代码如下
@Override
public void execute(Runnable task) {
...
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
}
所以这里每次循环开始都会把wakenUp标志清理掉。
-
判断当前任务队列是否有任务,如果有任务直接快速select一次以便能处理到任务。
-
如果任务队列没有任务则进行阻塞select并让出cup时间片。
-
如果wakenUp是true,则中断select一次。
这里会有个迷惑,就是如果上次加入任务中断了select一次这里状态还未清理还会再中断一次,这样重复中断设计的意义是什么?看官方说法是如果没有这个操作,第一次select被中断后等待任务执行过程中的所有加入的任务都不能改变wakenUp的状态为ture,因为改变状态是用cas方式wakenUp.compareAndSet(false, true),所以下次select会出现不必要的阻塞。因此这里做了重复的唤醒。这样做在没任务加入情况下其实是浪费的所以官方称这种做法inefficient。
- 处理io事件和根据占时比例配置处理任务
这里配置比例主要是让用户协调io事件和任务执行的时间,如果ioRatio配置100,会先执行io事件然后执行全部的任务;默认ioRatio配置50,会先执行io事件然后用io事件50%执行时间处理任务,处理任务的时间计算公式如下:
io事件处理时间 * (100 - ioRatio) / ioRatio
下面来看下select的实现:
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
//select计数器
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
//根据定时任务计算select延迟时间
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
//计算select阻塞时间
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
//阻塞式select
int selectedKeys = selector.select(timeoutMillis);
//计时器加1
selectCnt ++;
//如果发现待处理io事件或老唤醒标记true或最新唤醒标记为true或队列中有任务或有定时任务,跳出循环,中断本次轮询
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
//如果有线程被中断也,,跳出循环,中断本次轮询
if (Thread.interrupted()) {
selectCnt = 1;
break;
}
//如果的selet时间大于等于timeoutMillis,说明selet正常,计数器重归于1
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
selectCnt = 1;
//如果selet在timeoutMillis时间内返回次数大于配置次数说明可能触发了jdk的nio bug,则重建selector
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
rebuildSelector();
selector = this.selector;
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
} catch (CancelledKeyException e) {
}
}
select的操作是个循环,select会一直循环直到出现以下情况:
- 发现待处理io事件。
- 老唤醒标记true,说明处理IO接事件和其他业务事件期间加入了任务。
- 最新唤醒标记为true,说select期间加入了任务。
- 队列中有任务或有定时任务。
- 触发了jdk的nio bug。
select的操作主要有以下几个流程:
-
根据定时任务计算select延迟时间。 这里计算方式就取定时任务队列里的第一个任务(任务根据执行时间从小到大)获取执行时间加0.5毫秒,如果没有任务默认1秒加0.5毫秒。
-
阻塞式select。
-
如果发现待处理io事件或老唤醒标记true或最新唤醒标记为true或队列中有任务或有定时任务,跳出循环,中断本次轮询。
-
如果的select时间大于等于timeoutMillis,说明select正常,计数器重归于1。
-
如果select在timeoutMillis时间内返回次数大于配置次数说明可能触发了jdk的nio bug,则重建selector。 此bug会在没有IO事件发生时select立即返回,所以会造成无意义的循环最后可能导致cpu飙到100%情况,所以NIO采用计数器和重建selector方法解决这个bug
关于该bug的描述见 http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6595055
重建的过程就是先创建新的selector,将老的selector中监听key复制到新selector中,然后注册Channel到新selector中,最后关闭老的selector。
下面看下IO处理的实现
IO处理的实现就是先获取待处理的key,然后交给processSelectedKey方法去处理,我们看下processSelectedKey方法的实现
private static void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0){
unsafe.read();
if (!ch.isOpen()) {
return;
}
}
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
系统会根据SelectionKey处理哪种IO事件,IO事件共有4种:
- SelectionKey.OP_READ 读事件
- SelectionKey.OP_ACCEPT 接收事件
- SelectionKey.OP_WRITE 写事件
- SelectionKey.OP_CONNECT 连接事件
比如客户端链接后会触发服务端SelectionKey.OP_ACCEPT的接收事件,然后由服务端的主Channel处理,处理的过程后面会介绍。
处理完IO事件就是处理队列中的任务如果ioRatio配置成100比较简单处理方式就是一个一个执行队列里的全部任务,ioRatio非100处理比较复杂我们来下ioRatio非100的处理实现:
protected boolean runAllTasks(long timeoutNanos) {
//将需要触发的定时任务加入到任务队列
fetchFromScheduledTaskQueue();
//获取一个任务
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 ++;
//每64次任务进行一次,超时判断,如果超时退出执行
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;
}
执行任务的流程也是个循环直到超时或者没任务,执行任务流程如下:
-
将需要触发的定时任务加入到任务队列。
-
计算任务超时时间,超时时长是根据上面说的公式计算的。
-
执行任务。
-
每执行64次任务进行一次超时判断,如果超时退出执行。
这里设计64次的原因官方给的解释是每次调用nanoTime()去判断超时是耗费性能的,所以写成64,同时官方也表示硬编码成64是不太合理后面会改成可配置。
-
取下个任务,如果没有任务也退出执行。