Netty源码 reactor 模型
翻阅源码时,我们会发现netty中很多方法的调用都是通过线程池的方式进行异步的调用,
这种 eventLoop.execute
方式的调用,实际上便是reactor线程。对应项目中使用广泛的NioEventLoop。还记得我们创建的两个reactor线程池么,具体代码可以参考 Netty源码 服务端的启动
首先来解释下什么事 reactor 线程
Reactor模式是处理并发I/O比较常见的一种模式,用于同步I/O,中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;
一旦有I/O事件到来或是准备就绪(文件描述符或socket可读、写),多路复用器返回并将事先注册的相应I/O事件分发到对应的处理器中。
reactor线程的启动
io.netty.util.concurrent.SingleThreadEventExecutor#execute
reactor模型在第一次接受任务的时候,会启动线程
外部线程在提交任务时,netty会判断是否是当前前程是否是 SingleThreadEventExecutor中的Thread一致,如果不一致,说明需要启动一个新的线程接受任务。然后就会调用内部线程池执行reactor模型的run方法
然后就会执行addTask将线程封装成一个任务放到Queue中。Queue中的任务就是通过reactor线程来消费的。
reactor线程的执行
reactor线程做了三件事
1.不断的轮训注册到selector上channel的IO事件
2.处理IO事件,读取channel中的事件,选择一个work线程,准备执行任务
3.执行任务
上述三件事不断的轮训,下面我们依次进行分析。
1.select轮训
select轮训很简单,分为以下几步
1.延迟任务队列0.5秒以内有任务则中断
2.普通任务队列有任务添加则中断
3.阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有
- 轮询到IO事件 (
selectedKeys != 0
) - oldWakenUp 参数为true
- 任务队列里面有任务(
hasTasks
) - 第一个定时任务即将要被执行 (
hasScheduledTasks()
) - 用户主动唤醒(
wakenUp.get()
)
4.解决jdk空轮训bug,具体的bug我们可以看 https://www.jianshu.com/p/3ec120ca46b2
netty这边会记录每次轮训的时间,如果轮训的时间有效,累加器会加1,累加器到256之后,开始rebuildSelector,rebuildSelector
的操作其实很简单:new一个新的selector,将之前注册到老的selector上的的channel重新转移到新的selector上
Select步骤结束,表示轮训到了io事件,那么接下来我们就要去处理这些事件
2.处理IO事件
这里出现了一个selectedKeys,selectedKeys的类型是SelectedSelectionKeySet,其实也就是一个Set集合
private SelectedSelectionKeySet selectedKeys;
这里的SelectionKey又是什么呢,我们可以看下类注释
A selection key is created each time a channel is registered with a selector
每一个channel在向selector注册时都会创建一个SelectionKey。
暂且我们先认为 SelectionKey里面包含了通道注册时的一些信息。
现在我们开始处理io事件
private void processSelectedKeysOptimized(SelectionKey[] selectedKeys) { for (int i = 0;; i ++) { // 1.取出IO事件以及对应的channel final SelectionKey k = selectedKeys[i]; if (k == null) { break; } selectedKeys[i] = null; final Object a = k.attachment(); // 2.处理该channel if (a instanceof AbstractNioChannel) { processSelectedKey(k, (AbstractNioChannel) a); } else { NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a; processSelectedKey(k, task); } // 3.判断是否该再来次轮询 if (needsToSelectAgain) { for (;;) { i++; if (selectedKeys[i] == null) { break; } selectedKeys[i] = null; } selectAgain(); selectedKeys = this.selectedKeys.flip(); i = -1; } } }
这里的k.attachment()能转换成
AbstractNioChannel。
搜一下k.attach的调用关系,在io.netty.channel.nio.AbstractNioChannel#doRegister发现了如下代码,这能解释了selectionKey的创建
selectionKey = javaChannel().register(eventLoop().selector, 0, this);
public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException { synchronized (regLock) { if (!isOpen()) throw new ClosedChannelException(); if ((ops & ~validOps()) != 0) throw new IllegalArgumentException(); if (blocking) throw new IllegalBlockingModeException(); SelectionKey k = findKey(sel); if (k != null) { k.interestOps(ops); k.attach(att); } if (k == null) { // New registration synchronized (keyLock) { if (!isOpen()) throw new ClosedChannelException(); k = ((AbstractSelector)sel).register(this, ops, att); addKey(k); } } return k; }
我们现在再来看processSelectedKey方法,代码精简后实际上就是调用unsafe对不同的事件进行处理
3.处理任务队列
netty中总共有三种任务类型
1.普通的eventLoop任务
channel.eventLoop().execute(new Runnable() { @Override public void run() { //task.... } });
execute并没有真正去执行,而是将任务进行了封装。
public void execute(Runnable task) { if (task == null) { throw new NullPointerException("task"); } boolean inEventLoop = inEventLoop(); if (inEventLoop) { addTask(task); } else { startThread(); addTask(task); if (isShutdown() && removeTask(task)) { reject(); } } if (!addTaskWakesUp && wakesUpForTask(task)) { wakeup(inEventLoop); } }
最终我们的任务添加到了一个任务队列中
protected void addTask(Runnable task) { if (task == null) { throw new NullPointerException("task"); } if (!offerTask(task)) { reject(task); } } final boolean offerTask(Runnable task) { if (isShutdown()) { reject(); } return taskQueue.offer(task); }
这里 taskQueue并不是普通的任务队列,而是Mpsc队列,即多生产者单消费者队列,netty使用mpsc,方便的将外部线程的task聚集,在reactor线程内部用单线程来串行执行
protected Queue<Runnable> newTaskQueue(int maxPendingTasks) { // This event loop never calls takeTask() return PlatformDependent.newMpscQueue(maxPendingTasks); }
2.外部任务
服务端在接收到客户端请求时,需要选择相应的channel写数据到客户端
ctx.channel().writeAndFlush(responsePacket);
这种在用户线程中的任务最终同样会被封装到任务队列,channel的write最终代码在io.netty.channel.AbstractChannelHandlerContext#write(java.lang.Object, boolean, io.netty.channel.ChannelPromise)
private void write(Object msg, boolean flush, ChannelPromise promise) { 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); } }
先判断是否在eventloop线程,这里是false,最终封装成一个task,执行safeExecutor
private static void safeExecute(EventExecutor executor, Runnable runnable, ChannelPromise promise, Object msg) { try { executor.execute(runnable); } catch (Throwable cause) { try { promise.setFailure(cause); } finally { if (msg != null) { ReferenceCountUtil.release(msg); } } } }
接下来就和第一种情况一样,添加到队列当做。
3.定时任务
第三种场景就是定时任务逻辑,类似如下
eventLoop().schedule(new Runnable() { @Override public void run() { }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
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; }
这里的scheduledTaskQueue是一个优先级队列,注意这里的线程安全问题,如果不是在eventloop线程提交的,那么就会把添加操作封装成一个task,这个task的任务是添加[添加定时任务]的任务,而不是添加定时任务,其实也就是第二种场景,这样,对 PriorityQueue
的访问就变成单线程,即只有reactor线程
Queue<ScheduledFutureTask<?>> scheduledTaskQueue() { if (scheduledTaskQueue == null) { scheduledTaskQueue = new PriorityQueue<ScheduledFutureTask<?>>(); } return scheduledTaskQueue; }
现在再看runAllTasks方法
分为以下3步
- 从scheduledTaskQueue转移定时任务到taskQueue
- 计算本次任务循环的截止时间并执行
- 执行完成任务后的任务
代码还是相当清晰的。这里不再深入。
最后我们再来总结下,reactor模型实质上就干了三件事情,首先他会不停的检测是否有io事件发生或者 是否有任务快要发生,如果检测到了,说明他要去干活了。首先先去处理io事件,所有的io事件都是通过unsafe去处理。处理完io事件后便开始处理任务队列里面的队列。
以上关于reactor模型的研究。