阅读 767

Netty源码分析——Reactor的task

Netty源码分析——Reactor的task

前言

这是关于Reactor的最后一篇文章。主要看下Reacotr线程如何处理各种任务。Reactor中的任务主要分三种,分别是用户定义的普通任务、用户定义的定时任务和非当前Reactor线程调用channel的各种方法。

普通任务

很简单,例子就不看了,直接调用EventLoop的execute即可。看代码(还是NioEventLoop):

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

//这里是唤醒selector的阻塞select,之前文章中已经介绍过了
if (!addTaskWakesUp && wakesUpForTask(task)) {
wakeup(inEventLoop);
}
复制代码

直接添加任务,如果是外部调用(!inEventLoop()),开启线程。

这里我们看看这个addTask,放到哪里去了,最后会追到这么一句taskQueue.offer(task),这个队列实际的生成是:MpscUnboundedArrayQueue或者MpscUnboundedAtomicArrayQueue,Netty中默认试用的是一种Mpsc队列,意思是多生产者,单消费者。

非当前Reactor调用的channel任务

这种是代指形如:channel.write之类的操作,这里的write会调用pipeline.write,依赖AbstractChannelHandlerContext去传递,最终走到这里:

AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
//外部调用会走到这里
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
复制代码

这里会把write操作封装成一个AbstractWriteTask,我们先看看这个生成task的WriteTask.newInstance

WriteTask task = RECYCLER.get();
init(task, ctx, msg, promise);
return task;

//RECYCLER.get()
if (maxCapacityPerThread == 0) {
return newObject((Handle<T>) NOOP_HANDLE);
}
Stack<T> stack = threadLocal.get();
DefaultHandle<T> handle = stack.pop();
if (handle == null) {
handle = stack.newHandle();
handle.value = newObject(handle);
}
return (T) handle.value;
复制代码

如果maxCapacityPerThread不为0的时候,先获取一个stack,然后pop一个handler,handler如果有,就直接取handler的值,如果没有就初始化handler和我们要的对象(这里是WriteTask)。

我们看看DefaultHandle的实现:

static final class DefaultHandle<T> implements Handle<T> {
private int lastRecycledId;
private int recycleId;

boolean hasBeenRecycled;

private Stack<?> stack;
private Object value;

DefaultHandle(Stack<?> stack) {
this.stack = stack;
}

@Override
public void recycle(Object object) {
if (object != value) {
throw new IllegalArgumentException("object does not belong to handle");
}
stack.push(this);
}
}
复制代码

这里的recycle,就是把这个handler放回stack里,这时候其实就是维护了一个thread->stack-handler->value的链路,防止value被回收,实现了所谓的回收

这是题外话,收一下继续看execute write task的流程,执行safeExecuteexecutor.execute(runnable);。其实就是扔进NioEventLoop里执行,只不过这里结合我们说的第一种情况,用于调用channel.write可能是在很多线程中,这时候Netty的MPSC队列就起作用了,能够保证任务的原子的提交。

用户自定义定时任务

例子也不介绍了,直接看一下schedule方法:return schedule(new ScheduledFutureTask<Void>(this, command, null, ScheduledFutureTask.deadlineNanos(unit.toNanos(delay)));,继续跟进去:

if (inEventLoop()) {
scheduledTaskQueue().add(task);
} else {
//如果用户执行,则二次封装
execute(new Runnable() {
@Override
public void run() {
scheduledTaskQueue().add(task);
}
});
}
return task;
复制代码

最终都是在执行scheduledTaskQueue().add(task);,看看这个队列是啥:

PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue() {
if (scheduledTaskQueue == null) {
scheduledTaskQueue = new DefaultPriorityQueue<ScheduledFutureTask<?>>(
SCHEDULED_FUTURE_TASK_COMPARATOR,11);
}
return scheduledTaskQueue;
}
复制代码

初始化了一个默认的优先级队列,之前我们提到过,这个优先级队列,是使用定时任务的开始时间来排序的,最先快开始的就放在前面。

需要注意的是,这里可能是用户调用,是有可能并发的,但是没有使用有锁的队列。为什么不使用有锁队列?

看源码,这里Netty是把任务又封装成一个runnable来执行(看上面注释)。这样就形成了什么呢?

Netty执行一个任务(普通任务),这个任务的内容是向优先级队列里添加任务。这里因为是普通任务,会先进入MPSC队列,被取出来以后再由Reactor添加到优先级队列里(这里就是单线程操作了)。

我们看下ScheduledFutureTask是如何比较大小的:

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;
}
//截止时间相同再去比较id
else if (id < that.id) {
return -1;
} else if (id == that.id) {
throw new Error();
} else {
return 1;
}
}
复制代码

首先先比较任务的截止时间,截止时间相同的情况下,再比较id,即任务添加的顺序,如果id再相同的话,就抛Error。如此保证最近要开始的任务最先和先添加的任务优先执行。

run all tasks

看完三种任务,回归到Reactor线程中,执行任务的代码在:

final long ioTime = System.nanoTime() - ioStartTime;
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
复制代码

这段是放在finally块中的,Netty保证你的任务一定会执行。Netty默认情况下,保证处理IO和处理任务的时间是一致的,用来保证不单方面去执行IO或者任务,runAllTasks方法的入参表示尽量在一定的时间内,将所有的任务都取出来run一遍。

跟进去:

//把定时任务从scheduledTaskQueue取出放到taskQueue里
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 ++;
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;
复制代码

(runTasks & 0x3F) == 0是指:每隔0x3F任务,即每执行完64个任务之后,判断当前时间是否超过本次Reactor任务循环的截止时间了,如果超过,那就break掉,如果没有超过,那就继续执行。主要是考虑到任务太多的情况下,每次对比超时时间非常浪费性能。

执行之后再去执行收尾任务,这个收尾任务并非Netty的任务,而是用户通过executeAfterEventLoopIteration这个方法添加的任务。目前用的比较少,这个方法上也打了注解,标识这个方法是不稳定的、可能会被移除的。这里就不展开看了。

至此,整个Reactor线程做的任务我们就全部讲完了,希望大家能对Reacotr有一个比较全面的了解和认识~

文章分类
后端