3-netty源码分析之Reactor
首先这里用一个图来简单描述下netty的线程模型
其实这里的NioEventLoop就是主要讲的是reactor线程模型,如上图所示,该线程在一个无线死循环里主要做了三件事:
- 1.select轮训出就绪的IO事件
- 2.处理轮训出的IO事件
- 3.处理task
那么下面详细记录这三个动作是怎么处理的,当然这里bossGroup与workGroup动作一样,但做的事情前面介绍过。
看看核心代码【NioEventLoop#run】
/**
* Netty 的事件循环机制
* 当 taskQueue 中没有任务时, 那么 Netty 可以阻塞地等待 IO 就绪事件;
* 而当 taskQueue 中有任务时, 我们自然地希望所提交的任务可以尽快地执行, 因此 Netty 会调用非阻塞的 selectNow() 方法, 以保证 taskQueue 中的任务尽快可以执行.
*
* 1.轮询IO事件
* 2.处理轮询到的事件
* 3.执行任务队列中的任务
* */
@Override
protected void run() {
for (;;) {
try {
/** 如果任务队列没有任务,则进行一次selectNow() */
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
/**
* 轮询出注册在selector上面的IO事件
*
* wakenUp 表示是否应该唤醒正在阻塞的select操作,
* 可以看到netty在进行一次新的loop之前,都会将wakeUp 被设置成false,标志新的一轮loop的开始
*/
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
// fall through
default:
}
cancelledKeys = 0;
/**
* 第一步是通过 select/selectNow 调用查询当前是否有就绪的 IO 事件. 那么当有 IO 事件就绪时, 第二步自然就是处理这些 IO 事件啦.
*/
needsToSelectAgain = false;
/**
* 此线程分配给 IO 操作所占的时间比
* 即运行 processSelectedKeys 耗时在整个循环中所占用的时间
*/
final int ioRatio = this.ioRatio;
/** 当 ioRatio 为 100 时, Netty 就不考虑 IO 耗时的占比, 而是分别调用 processSelectedKeys()、runAllTasks(); */
if (ioRatio == 100) {
try {
/** 查询就绪的 IO 事件后 进行处理 */
processSelectedKeys();
} finally {
// Ensure we always run tasks.
/** 运行 taskQueue 中的任务. */
runAllTasks();
}
}
/**
* ioRatio 不为 100时, 则执行到 else 分支, 在这个分支中, 首先记录下 processSelectedKeys() 所执行的时间(即 IO 操作的耗时),
* 然后根据公式, 计算出执行 task 所占用的时间, 然后以此为参数, 调用 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);
}
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
这个方法怎么触发的可以直接看前面的文章,理解了NioEventLoop是一个线程,当提交任务时会进行start,就会触发这里的run循环了。
那么详细分解途中的三个主要功能。
1.select轮训就绪的IO事件
可以看到switch条件体:当前任务队列中没有任务时,进行一次selectNow操作,selectNow操作是无阻塞操作,会立刻返回,而select(time)是阻塞操作。
@Override
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}
private final IntSupplier selectNowSupplier = new IntSupplier() {
@Override
public int get() throws Exception {
return selectNow();
}
};
可以看到代码中select不会获取返回值,那么这里netty怎么去知道已经有就绪的IO事件呢?既然不获取返回值,那么必然是select操作中操作了某些标识IO事件就绪的变量,这里暂时留个疑问,先看处理就绪的IO事件后再回来一探究竟填充了什么用以标识IO事件已就绪。这里暂时只要知道select操作的就是轮训就绪的IO事件就好。
既然select是轮训,那么我们详细看下轮训逻辑
/**
* 如果发现当前的定时任务队列中有任务的截止事件快到了(<=0.5ms),就跳出循环。
* 此外,跳出之前如果发现目前为止还没有进行过select操作(if (selectCnt == 0)),那么就调用一次selectNow(),该方法会立即返回,不会阻塞
*/
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0;
long currentTimeNanos = System.nanoTime();
/**
* netty里面定时任务队列是按照延迟时间从小到大进行排序,
* delayNanos(currentTimeNanos)方法即取出第一个定时任务的延迟时间
*/
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
/**
* 1.定时任务截至事时间快到了,中断本次轮询
*/
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
selector.selectNow();
selectCnt = 1;
}
break;
}
/**
* 2.轮询过程中发现有任务加入,中断本次轮询
*/
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
selector.selectNow();
selectCnt = 1;
break;
}
/**
* 3.阻塞式select操作
* 为了保证任务队列能够及时执行,在进行阻塞select操作的时候会判断任务队列是否为空,如果不为空,就执行一次非阻塞select操作,跳出循环
*
* 到来这里, 我们可以看到, 当 hasTasks() 为真时, 调用的的 selectNow() 方法是不会阻塞当前线程的, 而当 hasTasks() 为假时, 调用的 select(oldWakenUp) 是会阻塞当前线程的.
* 执行到这一步,说明netty任务队列里面队列为空,并且所有定时任务延迟时间还未到(大于0.5ms),于是,在这里进行一次阻塞select操作,截止到第一个定时任务的截止时间
*/
int selectedKeys = selector.select(timeoutMillis);
selectCnt ++;
/**
* 阻塞select操作结束之后,netty又做了一系列的状态判断来决定是否中断本次轮询,中断本次轮询的条件有
*/
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
if (Thread.interrupted()) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely because " +
"Thread.currentThread().interrupt() was called. Use " +
"NioEventLoop.shutdownGracefully() to shutdown the NioEventLoop.");
}
selectCnt = 1;
break;
}
/**
* 4.解决jdk的nio bug
* 记录select空转的次数,定义一个阀值,这个阀值默认是512,可以在应用层通过设置系统属性io.netty.selectorAutoRebuildThreshold传入,
* 当空转的次数超过了这个阀值,重新构建新Selector,将老Selector上注册的Channel转移到新建的Selector上,关闭老Selector,用新的Selector代替老Selector,详细实现可以查看NioEventLoop中的selector和rebuildSelector方法:
*/
long time = System.nanoTime();
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", selectCnt, selector);
rebuildSelector();
selector = this.selector;
// Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS) {
if (logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.", selectCnt - 1, selector);
}
}
} catch (CancelledKeyException e) {
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?", selector, e);
}
// Harmless exception - log anyway
}
}
核心逻辑就是调用selector的select逻辑去轮训。
那么我们知道这里轮训 跟后面的处理事件是一个线程,总不能一直在这死循环吧,那么什么时候会中断轮训呢?
其实上面代码已经很好的表达了中断逻辑:
- 1.发现当前的定时任务队列中有任务的截止事件快到了(<=0.5ms)
- 2.selectedKeys != 0
- 3.oldWakenUp==true
- 4.hasTasks()
- 5.hasScheduledTasks()
- 6.wakenUp.get()
以上任何一个条件满足就会中断轮训,但只有第一个条件满足时,还会无阻塞selectNow
一次,可以看到这个轮训的处理优越之处。
其实这里还有个核心点:
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.
logger.warn("Selector.select() returned prematurely {} times in a row; rebuilding Selector {}.", selectCnt, selector);
rebuildSelector();
selector = this.selector;
// Select again to populate selectedKeys.
selector.selectNow();
selectCnt = 1;
break;
}
这里利用重新构建Selector的方式解决了jdk 的nio 空轮训的bug。那么什么时候重建呢? 就是设置netty的最大空轮训次数,默认是512,达到这个数后,就会重建Selector了。
2.processSelectedKeys处理就绪的IO事件
从代码中可以看出,select后紧接着就是处理了,因为上面select没有返回值,那么这里处理必然有两种情况:
- 1.没有就绪的IO事件,直接返回。
- 2.有就绪的IO事件,处理。
那么看看究竟是否如此:
private void processSelectedKeys() {
if (selectedKeys != null) {
/** 处理优化过的selectedKeys */
processSelectedKeysOptimized();
} else {
/** 正常的处理 */
processSelectedKeysPlain(selector.selectedKeys());
}
}
/**
* 迭代 selectedKeys 获取就绪的 IO 事件, 然后为每个事件都调用 processSelectedKey 来处理它.
*
* 1.对于boss NioEventLoop来说,轮询到的是基本上就是连接事件,后续的事情就通过他的pipeline将连接扔给一个worker NioEventLoop处理
* 2.对于worker NioEventLoop来说,轮询到的基本上都是io读写事件,后续的事情就是通过他的pipeline将读取到的字节流传递给每个channelHandler来处理
*/
private void processSelectedKeysOptimized() {
for (int i = 0; i < selectedKeys.size; ++i) {
/** 1.取出IO事件以及对应的channel */
final SelectionKey k = selectedKeys.keys[i];
// null out entry in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
/** 取出后将数组置为空 */
selectedKeys.keys[i] = null;
final Object a = k.attachment();
/** 处理该channel */
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask task = (NioTask) a;
processSelectedKey(k, task);
}
/**
* 判断是否该再来次轮询
* 也就是说,对于每个NioEventLoop而言,每隔256个channel从selector上移除的时候,就标记 needsToSelectAgain 为true,我们还是跳回到上面这段代码
*/
if (needsToSelectAgain) {
// null out entries in the array to allow to have it GC'ed once the Channel close
// See https://github.com/netty/netty/issues/2363
/** 将selectedKeys的内部数组全部清空 */
selectedKeys.reset(i + 1);
/** 重新调用selectAgain重新填装一下 selectionKey */
selectAgain();
i = -1;
}
}
}
上面两端代码直接就是处理keys了,可以看到有两个入口,一个是优化的,一个是正常的。那么这里介绍优化的处理;其实这里优化的是什么呢?
可以看到第二段代码中,for循环做的就是将selectedKeys中的遍历进行处理,那么验证前面的猜想,select是不是就是填充了这个selectedKeys数组?先继续这个处理逻辑,后面回头进行验证这一点。
其实这里已经验证了一个猜想,当没有就绪的IO事件时直接返回。
其次,当有就绪的IO事件时,直接调用下面逻辑处理:
if (a instanceof AbstractNioChannel) {
processSelectedKey(k, (AbstractNioChannel) a);
}
有个疑问哈,那么这里的attachment怎么就是AbstractNioChannel呢?
在server端启动时,我们看这段逻辑:
@Override
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
/** 将 Channel 对应的 Java NIO SockerChannel 注册到一个 eventLoop 的 Selector 中, 并且将当前 Channel 作为 attachment. */
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
}
}
}
继续跟踪到java nio中的逻辑:
public final SelectionKey register(Selector sel, int ops,Object att)
throws ClosedChannelException
{
synchronized (regLock) {
...
if (k != null) {
k.interestOps(ops);
k.attach(att);
}
...
return k;
}
}
这里就会发现,在server启动注册时,就会将this 即 AbstractNioChannel 作为attachment保存在selectionKey中,这样轮训出事件后,就可以直接取出对应的channel使用了。
那么继续回到processSelectedKeys这个核心方法:
/**
* processSelectedKey 中处理了三个事件, 分别是:
* 1.OP_READ, 可读事件, 即 Channel 中收到了新数据可供上层读取.
* 2.OP_WRITE, 可写事件, 即上层可以向 Channel 写入数据.
* 3.OP_CONNECT, 连接建立事件, 即 TCP 连接已经建立, Channel 处于 active 状态.
* 4.OP_ACCEPT,请求连接事件
*/
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
// If the channel implementation throws an exception because there is no event loop, we ignore this
// because we are only trying to determine if ch is registered to this event loop and thus has authority
// to close ch.
return;
}
// Only close ch if ch is still registered to this EventLoop. ch could have deregistered from the event loop
// and thus the SelectionKey could be cancelled as part of the deregistration process, but the channel is
// still healthy and should not be closed.
// See https://github.com/netty/netty/issues/5125
if (eventLoop != this || eventLoop == null) {
return;
}
// close the channel if the key is not valid anymore
unsafe.close(unsafe.voidPromise());
return;
}
try {
int readyOps = k.readyOps();
// We first need to call finishConnect() before try to trigger a read(...) or write(...) as otherwise
// the NIO JDK channel implementation may throw a NotYetConnectedException.
/**
* 事件是 OP_CONNECT, 即 TCP 连接已建立事件.
* 1.我们需要将 OP_CONNECT 从就绪事件集中清除, 不然会一直有 OP_CONNECT 事件.
* 2.调用 unsafe.finishConnect() 通知上层连接已建立
* */
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
// remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking
// See https://github.com/netty/netty/issues/924
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
/**
* unsafe.finishConnect() 调用最后会调用到 pipeline().
* fireChannelActive(), 产生一个 inbound 事件, 通知 pipeline 中的各个 handler TCP 通道已建立(即 ChannelInboundHandler.channelActive 方法会被调用)
*/
unsafe.finishConnect();
}
// Process OP_WRITE first as we may be able to write some queued buffers and so free memory.
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// Call forceFlush which will also take care of clear the OP_WRITE once there is nothing left to write
ch.unsafe().forceFlush();
}
// Also check for readOps of 0 to workaround possible JDK bug which may otherwise lead
// to a spin loop
/** boos reactor处理新的连接 或者 worker reactor 处理 已存在的连接有数据可读 */
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
/** AbstractNioByteChannel中实现,重点 */
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
其实已经出现了想要的东西了,IO事件分为以下几种,
- 1.OP_READ, 可读事件, 即 Channel 中收到了新数据可供上层读取.
- 2.OP_WRITE, 可写事件, 即上层可以向 Channel 写入数据.
- 3.OP_CONNECT, 连接建立事件, 即 TCP 连接已经建立, Channel 处于 active 状态.
- 4.OP_ACCEPT,请求连接事件
那么上一段代码就是从k.readyOps();获取对应的就绪事件,然后进行对应的处理;其实前面就已经指出,netty最终的读写及连接等底层相关的操作都是交由UnSafe去处理的,那么这里也就是最终也是如此。这里拿连接请求举例:
SelectionKey.OP_ACCEPT事件对应的处理逻辑就是
unsafe.read();
netty 与bossGroup对应的UnSafe就是NioMessageUnsafe这个了,专门处理连接相关的IO事件,从上面的read进去就会到一下逻辑:
/**
* 服务端需要的UnSafe对象
* 将新的连接注册到worker线程组
*
* netty将一个新连接的建立也当作一个io操作来处理,这里的Message的含义我们可以当作是一个SelectableChannel,读的意思就是accept一个SelectableChannel,写的意思是针对一些无连接的协议,比如UDP来操作的
*/
private final class NioMessageUnsafe extends AbstractNioUnsafe {
private final List
该逻辑就是具体的连接请求处理了,该篇主要讲的是reactor,所以这里不做展开。
OK,到此,processSelectedKeys的大体逻辑就分析完了,那么回头就解决下前面的疑问:selectedKeys是不是select阶段填充的?
那么我们继续,我们先看下selectedKeys的结构:
private SelectedSelectionKeySet selectedKeys;
final class SelectedSelectionKeySet extends AbstractSet {
SelectionKey[] keys;
int size;
SelectedSelectionKeySet() {
keys = new SelectionKey[1024];
}
/**
* 判断当前使用哪个数组,找到对应的数组,然后经历下面三个步骤
* 1.将SelectionKey塞到该数组的逻辑尾部
* 2.更新该数组的逻辑长度+1
* 3.如果该数组的逻辑长度等于数组的物理长度,就将该数组扩容
*/
@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}
keys[size++] = o;
if (size == keys.length) {
increaseCapacity();
}
return true;
}
}
可以看到SelectedSelectionKeySet是一个AbstractSet的扩展类,它的核心逻辑add方法被重写了,显然性能用数组体现了,那么这里也就是点明processSelectedKeysOptimized为何叫优化处理了。
那么这里就debug到这个add方法,通过最终调用栈形式,看看是不是select方法会触发这里的SelectionKey数组的add逻辑。
跟踪调用栈:
果然会进行add操作,那么前面的猜想就是对的:select操作中如果轮训到就绪的IO事件,就会在这里填充这个SelectionKey数组,在processSelectedKeys时就会拿出来处理了。
继续向西分析一下这里:
我们继续抱着疑问猜想一下:
既然能够到SelectedSelectionKeySet的add逻辑,那么这里是怎么取到的呢,根据上面的debug图示,结果确实是如此,那么我们就跟踪下设置的源头。
NioEventLoop(NioEventLoopGroup parent, ThreadFactory threadFactory, SelectorProvider selectorProvider, SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler) {
...
final SelectorTuple selectorTuple = openSelector();
selector = selectorTuple.selector;
...
}
private SelectorTuple openSelector() {
final Selector unwrappedSelector;
try {
unwrappedSelector = provider.openSelector();
} catch (IOException e) {
throw new ChannelException("failed to open a new selector", e);
}
if (DISABLE_KEYSET_OPTIMIZATION) {
return new SelectorTuple(unwrappedSelector);
}
/** selectedKeys是一个 SelectedSelectionKeySet 类对象,在NioEventLoop 的 openSelector 方法中创建 */
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
/** 通过反射将selectedKeys与 sun.nio.ch.SelectorImpl 中的两个field绑定 */
Object maybeSelectorImplClass = AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Object run() {
try {
return Class.forName(
"sun.nio.ch.SelectorImpl",
false,
PlatformDependent.getSystemClassLoader());
} catch (Throwable cause) {
return cause;
}
}
});
if (!(maybeSelectorImplClass instanceof Class) ||
// ensure the current selector implementation is what we can instrument.
!((Class>) maybeSelectorImplClass).isAssignableFrom(unwrappedSelector.getClass())) {
if (maybeSelectorImplClass instanceof Throwable) {
Throwable t = (Throwable) maybeSelectorImplClass;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, t);
}
return new SelectorTuple(unwrappedSelector);
}
final Class> selectorImplClass = (Class>) maybeSelectorImplClass;
Object maybeException = AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Object run() {
try {
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
}
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
}
}
});
if (maybeException instanceof Exception) {
selectedKeys = null;
Exception e = (Exception) maybeException;
logger.trace("failed to instrument a special java.util.Set into: {}", unwrappedSelector, e);
return new SelectorTuple(unwrappedSelector);
}
selectedKeys = selectedKeySet;
logger.trace("instrumented a special java.util.Set into: {}", unwrappedSelector);
return new SelectorTuple(unwrappedSelector,
new SelectedSelectionKeySetSelector(unwrappedSelector, selectedKeySet));
}
順便附上一张debug的图:
那么可以看到,在server启动时,初始化EventLoop时,会进行相关的设置,通过上叙带么就可以看出此处的类型确实是如此,这也为后续的轮训填充数组铺垫了。
上面的代码没有精简,因为还有用处,我们继续回到add逻辑的调用栈:
可以看到这里select操作的是java nio包中的逻辑,那么这里是怎么将轮训的结果替换成netty自身的SelectedSelectionKeySet中的数组的呢?
final SelectedSelectionKeySet selectedKeySet = new SelectedSelectionKeySet();
通过上面代码中可以看到成功替换了selectedKeys,再通过栈发现:
这里就自然触发了相关的逻辑,成功替换了赋值操作。那么这里怎么成功替换了jdk的轮训的结果呢?
Object maybeException = AccessController.doPrivileged(new PrivilegedAction() {
@Override
public Object run() {
try {
/**
* 通过反射将selectedKeys与 sun.nio.ch.SelectorImpl 中的两个field绑定
* selector在调用select()族方法的时候,如果有IO事件发生,就会往里面的两个field中塞相应的selectionKey(具体怎么塞有待研究),即相当于往一个hashSet中add元素
* */
Field selectedKeysField = selectorImplClass.getDeclaredField("selectedKeys");
Field publicSelectedKeysField = selectorImplClass.getDeclaredField("publicSelectedKeys");
Throwable cause = ReflectionUtil.trySetAccessible(selectedKeysField, true);
if (cause != null) {
return cause;
}
cause = ReflectionUtil.trySetAccessible(publicSelectedKeysField, true);
if (cause != null) {
return cause;
}
selectedKeysField.set(unwrappedSelector, selectedKeySet);
publicSelectedKeysField.set(unwrappedSelector, selectedKeySet);
return null;
} catch (NoSuchFieldException e) {
return e;
} catch (IllegalAccessException e) {
return e;
}
}
});
可以看到这段逻辑,netty用利用反射将自身的selectedKeySet将sun.nio.ch.SelectorImpl中的相关field替换了,因此在jdk nio轮训出事件时,就可以进行相应的截获了,可以这么理解吧,那么IO事件一旦发生,就可以在相关的set中add元素了。
OK,疑惑到这里也就相信解释清楚了,select --> processSelectedKeys就到此结束。
3.run task 处理任务
/** 当 ioRatio 为 100 时, Netty 就不考虑 IO 耗时的占比, 而是分别调用 processSelectedKeys()、runAllTasks(); */
if (ioRatio == 100) {
try {
/** 查询就绪的 IO 事件后 进行处理 */
processSelectedKeys();
} finally {
// Ensure we always run tasks.
/** 运行 taskQueue 中的任务. */
runAllTasks();
}
}
/**
* ioRatio 不为 100时, 则执行到 else 分支, 在这个分支中, 首先记录下 processSelectedKeys() 所执行的时间(即 IO 操作的耗时),
* 然后根据公式, 计算出执行 task 所占用的时间, 然后以此为参数, 调用 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);
}
}
从这段代码中我们可以看到,netty为任务的处理设定了一个有效时间,这种处理逻辑也就是防止task执行时间太久而导致阻塞了IO事件的轮训,这也体现了netty对于线程性能考虑的全面。
那么我们继续跟踪
runAllTasks();
protected boolean runAllTasks() {
boolean fetchedAll;
do {
/** 1.将 scheduledTaskQueue 中已经可以执行的(即定时时间已到的 schedule 任务) 拿出来并添加到 taskQueue 中, 作为可执行的 task 等待被调度执行 */
fetchedAll = fetchFromScheduledTaskQueue();
/** 2.从 taskQueue 中获取一个可执行的 task, 然后调用它的 run() 方法来运行此 task. */
Runnable task = pollTask();
if (task == null) {
return false;
}
for (;;) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception.", t);
}
task = pollTask();
if (task == null) {
break;
}
}
} while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
lastExecutionTime = ScheduledFutureTask.nanoTime();
return true;
}
上面这段逻辑句句核心,那么就翻译一下:
- 1.将scheduledTaskQueue中要到时间的定时任务取出,放到taskQueue「mpsc queue」中等待执行;
- 2.从 taskQueue 中获取一个可执行的 task
- 3.执行task
按照上叙翻译,分别贴一下执行代码,并且详细描述下:
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;
}
protected final Runnable pollScheduledTask(long nanoTime) {
assert inEventLoop();
Queue> 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;
}
可以看到这里将scheduledTaskQueue中要到时间的任务取出,重新offer进taskQueue中。那这里为什么要这么做?其实道理很简单,就是为了进行任务的收冗执行,也就是统一交由NioEventLoop线程去处理定时任务,做到资源的可控。
那么我们主要来看一下taskQueue的具体结构,找到初始化的地方:
/** mpsc队列:多生产者单消费者队列,这里使用mpsc,无非是将所有的task进行收冗执行,在netty内部用单线程来串行执行 */
@Override
protected Queue newTaskQueue(int maxPendingTasks) {
// This event loop never calls takeTask()
return maxPendingTasks == Integer.MAX_VALUE ? PlatformDependent.newMpscQueue()
: PlatformDependent.newMpscQueue(maxPendingTasks);
}
注释就很清楚MpscQueue就是一个多生产者,单消费者的队列,这里这么已处理其实也就避免了很多问题,比如:多个线程同时处理一个队列时,如果是普通队列就会存在竞争问题,那么这里进行收容执行后,就不会发生此类问题了。具体MpscQueue的结构可以去详细资料检索下。
最后task.run就是调用EventLoop去具体执行处理出队任务了,这里不多描述。
最后点明一下,这里的任务怎么来的呢:
@Override
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);
}
}
就是这个逻辑了,前面很多地方都有遇到,比如:
ch.eventLoop().execute(new Runnable() {
@Override
public void run() {
/** 这里的 childGroup.register 就是将 workerGroup 中的某个 EventLoop 和 NioSocketChannel 关联 */
pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
}
});
相信看了前面文章的就不会陌生。
SO,run task记录到这里。