从《netty源码阅读之NioEventLoop之NioEventLoop执行》知道,select之后,有两个步骤:
processSelectedKey()和runAllTask()
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
runAllTasks();
}
} else {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
// Ensure we always run tasks.
final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
这里有个ioRatio,就是执行io任务和非io任务的时间比。用户可以自行设置。默认为50,可以看源码。如果是100的话,先执行完io任务,执行完之后才执行非io任务。我们现在学习比较复杂的第二种情况。
在runAllTasks(long timeoutNanos)里面,主要分享以下三件事情:
1、task的分类和添加
2、任务的聚合
3、任务的真正执行
点进去runAllTasks(long timeoutNanos)方法的实现:
protected boolean runAllTasks(long timeoutNanos) {
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
final long deadline = ScheduledFutureTask.nanoTime() + timeoutNanos;
long runTasks = 0;
long lastExecutionTime;
for (;;) {
safeExecute(task);
runTasks ++;
// Check timeout every 64 tasks because nanoTime() is relatively expensive.
// XXX: Hard-coded value - will make it configurable if it is really a problem.
if ((runTasks & 0x3F) == 0) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
if (lastExecutionTime >= deadline) {
break;
}
}
task = pollTask();
if (task == null) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
break;
}
}
afterRunningAllTasks();
this.lastExecutionTime = lastExecutionTime;
return true;
}
第一步是把定时任务读出来, 第二步是从任务队列里面取任务,第三步是safeExecute()执行任务,通过(runTasks & 0x3F) == 0判断执行的任务次数是否达到64个,如果到达,那么就判断是否到达我们非io任务的时间,是的话就退出,并且记录下最后执行任务的时间lastExecutionTime,否则继续取任务去执行。所有任务执行完了,会调用afterRunningAllTasks()进行收尾工作。
关于64,在源码里面也就解释,为什么不执行完一次任务检查一次?检查也是相对耗时的操作,这里他们用了硬编码的方式,如果实在需要,可以修改为可配置的。
我们首先分析fetchFromScheduledTaskQueue()
private boolean fetchFromScheduledTaskQueue() {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
Runnable scheduledTask = pollScheduledTask(nanoTime);
while (scheduledTask != null) {
if (!taskQueue.offer(scheduledTask)) {
// No space left in the task queue add it back to the scheduledTaskQueue so we pick it up again.
scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
scheduledTask = pollScheduledTask(nanoTime);
}
return true;
}
也就是每次从定时任务队列scheduledTaskQueue里面取出到期的任务,添加到我们taskQueue里面,这就是任务的聚合。如果添加不成功,也就是有可能taskQueue满了,就要添加回定时任务队列,否者,这个任务可能丢失。
在深入一点看pollScheduledTask():
protected final Runnable pollScheduledTask(long nanoTime) {
assert inEventLoop();
Queue<ScheduledFutureTask<?>> scheduledTaskQueue = this.scheduledTaskQueue;
ScheduledFutureTask<?> scheduledTask = scheduledTaskQueue == null ? null : scheduledTaskQueue.peek();
if (scheduledTask == null) {
return null;
}
if (scheduledTask.deadlineNanos() <= nanoTime) {
scheduledTaskQueue.remove();
return scheduledTask;
}
return null;
}
在这里可以看到,到期的定时任务会返回并从定时任务队列里面删除。最快到期的定时任务会在最前面,关于这个定时任务队列,我们继续看this.scheduledTaskQueue这个的实现:
Queue<ScheduledFutureTask<?>> scheduledTaskQueue() {
if (scheduledTaskQueue == null) {
scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>();
}
return scheduledTaskQueue;
}
使用了优先队列的实现,所以可以比较。而比较的方法在它的定时任务ScheduledFutureTask里面:
@SuppressWarnings("ComparableImplementedButEqualsNotOverridden")
final class ScheduledFutureTask<V> extends PromiseTask<V> implements ScheduledFuture<V> {
...
@Override
public int compareTo(Delayed o) {
if (this == o) {
return 0;
}
ScheduledFutureTask<?> that = (ScheduledFutureTask<?>) o;
long d = deadlineNanos() - that.deadlineNanos();
if (d < 0) {
return -1;
} else if (d > 0) {
return 1;
} else if (id < that.id) {
return -1;
} else if (id == that.id) {
throw new Error();
} else {
return 1;
}
}
...
}
可以看到,deadline时间最小的,是排在最前面的,如果deadline时间相同,那么就比较id,可以严格保证任务的先后顺序。
关于NioEventLoop的定时任务的实现,还有一个细节,在它父类AbstractScheduledEventExecutor的schedule方法里面:
<V> ScheduledFuture<V> schedule(final ScheduledFutureTask<V> task) {
if (inEventLoop()) {
scheduledTaskQueue().add(task);
} else {
execute(new Runnable() {
@Override
public void run() {
scheduledTaskQueue().add(task);
}
});
}
return task;
}
如果丢定时任务的线程是NioEventLoop的线程,那么就把它放到定时任务队列里面,这是添加定时任务,但是我们这里关注的是,如果不是NioEventLoop的线程,那就会调用execute方法,新建一个线程来把任务丢到定时任务队列,这个新建的线程最终会绑定到NioEventLoop,通过这种方式保证了线程的安全。细细体会作者这种方法,特别巧妙。
接下去就是Runnable task = pollTask();,进入这个方法,最终可以看到就是从我们的taskQueue里面把在最前面的任务取出来,这里的任务已经包括可能的定时任务了。
protected final Runnable pollTaskFrom(Queue<Runnable> taskQueue) {
for (;;) {
Runnable task = taskQueue.poll();
if (task == WAKEUP_TASK) {
continue;
}
return task;
}
}
最后是safeExecute(task),直接调用task的run方法:
/**
* Try to execute the given {@link Runnable} and just log if it throws a {@link Throwable}.
*/
protected static void safeExecute(Runnable task) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception. Task: {}", task, t);
}
}
如果执行错误,会打印日志,不影响别的任务的使用。有的源码的实现,一个任务不行,别的任务也都崩溃了。