Netty - NioEventLoop 源码解析(运行相关)
Netty - NioEventLoop 源码解析(启动相关)
NioEventLoop的构造方法这里就不说了,在上一篇种仔细介绍过。
这里再来回顾下他的 继承体系:
由上图可看出, 其继承了 ScheduledExecutorService 调度线程池接口 。
也就是说 该 NioEventLoop 单线程池除了能够处理 本地任务 和 Selector感兴趣的事件外,还可以处理 可调度任务
这里的调度线程池 与 ScheduleThreadPoolExecutor 的实现有些区别, 实际上它只是 提供了PriorityQueue 优先级队列 来存放可调度任务, 并不是 DelayQueue 延迟优先级队列 。也就是说 NioEventLoop 中做了相关的阻塞处理。
下面从提交任务的方法 execute(task) 方法来入手
1.提交任务
private void execute(Runnable task, boolean immediate) {
// 判断当前提交任务的线程是否是 EventLoop线程
boolean inEventLoop = inEventLoop();
// 加入到 本地任务队列 中
addTask(task);
// 条件成立: 当前线程不是 eventLoop线程
if (!inEventLoop) {
//开启 eventLoop线程
startThread();
// .... 省略
}
}
代码很简单明了:
- 将任务添加到本地任务队列中
- 判断当前线程是否是 eventLoop线程,若不是 则启动eventLoop线程
2.启动线程
private void startThread() {
// 条件成立: eventLoop 状态为 非启动状态
if (state == ST_NOT_STARTED) {
// CAS 设置 eventLoop 状态为 启动状态 (线程安全考虑)
if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {
boolean success = false;
try {
// 真正开启线程
doStartThread();
success = true;
} finally {
// 若启动过程中有异常,则将状态修改为 非启动状态
if (!success) {
STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);
}
}
}
}
}
上述代码 使用 state字段,通过CAS 来控制 eventLoop 的启动时的并发性, 而真正 启动线程的 代码就一行
doStartThread()。
private void doStartThread() {
assert thread == null;
// 向线程执行器提交 eventLoop启动任务,真正开启线程
executor.execute(new Runnable() {
@Override
public void run() {
// 将该线程 保存在 eventLoop 的 thread 字段中
thread = Thread.currentThread();
if (interrupted) {
thread.interrupt();
}
boolean success = false;
updateLastExecutionTime();
try {
// NioEventLoop线程 真正运行的核心逻辑
SingleThreadEventExecutor.this.run();
success = true;
} catch (Throwable t) {
logger.warn("Unexpected exception from an event executor: ", t);
} finally {
// .... 代码省略 处理当 eventLoop 启动异常的过程
}
}
});
}
从上述代码可知, NioEventLoop线程 实际上是通过 executor 来创建的,之后再将创建的线程 赋值给 NioEventLoop对象的 thread字段。
而NioEventLoop线程 真正的运行核心逻辑是在 SingleThreadEventExecutor.this.run(); 方法中
3.核心逻辑
追踪SingleThreadEventExecutor.this.run(); 该方法,会来到 NioEventLoop #run() 方法。
@Override
protected void run() {
// epoll bug 特征计数变量
// 每执行一次 select 后, 该值会 +1
int selectCnt = 0;
for (;;) {
try {
// 1. >= 0时 表示 selector上的事件就绪个数
// 2. < 0时 表示 CONTINUE(-2) BUSY_WAIT(-3) SELECT(-1)
int strategy;
// ----------------1. select 获取 selector 上的就绪事件(IO事件) ---------------------
try {
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
case SelectStrategy.SELECT:
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
try {
if (!hasTasks()) {
strategy = select(curDeadlineNanos);
}
} finally {
nextWakeupNanos.lazySet(AWAKE);
}
// fall through
default:
}
} catch (IOException e) {
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
//-----------------2. 处理IO事件 和 本地任务 (普通任务,调度任务) --------------------------
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
final int ioRatio = this.ioRatio;
boolean ranTasks;
if (ioRatio == 100) {
try {
if (strategy > 0) {
processSelectedKeys();
}
} finally {
ranTasks = runAllTasks();
}
} else if (strategy > 0) {
final long ioStartTime = System.nanoTime();
try {
processSelectedKeys();
} finally {
final long ioTime = System.nanoTime() - ioStartTime;
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
} else {
ranTasks = runAllTasks(0); // This will run the minimum number of tasks
}
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
selectCnt = 0;
} else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
selectCnt = 0;
}
}
// .... 下面 不是核心逻辑了 ..........
catch (CancelledKeyException e) {
// Harmless exception - log anyway
if (logger.isDebugEnabled()) {
logger.debug(CancelledKeyException.class.getSimpleName() + " raised by a Selector {} - JDK bug?",
selector, e);
}
} catch (Error e) {
throw (Error) e;
} catch (Throwable t) {
handleLoopException(t);
} finally {
// Always handle shutdown even if the loop processing threw an exception.
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return;
}
}
} catch (Error e) {
throw (Error) e;
} catch (Throwable t) {
handleLoopException(t);
}
}
}
}
上述代码很长, 我们主要分两部分来看, 我分别用 分割线(-----------------------) 在代码中隔开了, 从分割线上的注释能知道,这一大段代码主要的 功能如下:
1. select 获取 selector 上的就绪事件(IO事件)
2. 处理IO事件 和 本地任务 (普通任务,调度任务)
3.1 部分一
我们首先分析第一部分的代码
try {
// 根据当前NioEventLoop 是否有本地普通任务,来决定怎么处理
// 1. 有任务,调用多路复用器 selectNow() 方法 返回 多路复用器上 就绪ch个数
// 2. 没有任务 返回 -1(SELECT)
strategy = selectStrategy.calculateStrategy(selectNowSupplier, hasTasks());
switch (strategy) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.BUSY_WAIT:
// 当没有 本地普通任务 时,会来到该分支 ...
case SelectStrategy.SELECT:
// 计算 最近的 本地可调度任务 的截止时间
long curDeadlineNanos = nextScheduledTaskDeadlineNanos();
// 条件成立:没有 本地可调度任务,则 curDeadlineNanos = Long.MAX
if (curDeadlineNanos == -1L) {
curDeadlineNanos = NONE; // nothing on the calendar
}
nextWakeupNanos.set(curDeadlineNanos);
try {
// 条件成立: 没有 本地普通任务
if (!hasTasks()) {
// 则阻塞 select(time)
strategy = select(curDeadlineNanos);
}
} finally {
nextWakeupNanos.lazySet(AWAKE);
}
// fall through
// 当有本地任务时, 则会直接走到第二部分
default:
}
} catch (IOException e) {
rebuildSelector0();
selectCnt = 0;
handleLoopException(e);
continue;
}
上述代码 主要分为两种情况,(其中复杂点的为情况2):
- 有 本地普通任务 : 立即调用 selectNow() 获取就绪事件个数
- 没有 本地普通任务 :
- 获取 最近 本地调度任务 的执行时间点
- 若 有 本地调度任务 , 则定时阻塞select( time) 到 本地调度任务 该执行的时间点
- 若 没有 本地调度任务 ,则一直阻塞 select() ,直到有 就绪事件到来为止.
3.2 部分二
// 到此 上面是 做过select操作了,因此 selectCnt+1
selectCnt++;
cancelledKeys = 0;
needsToSelectAgain = false;
// 表示下面 处理 IO事件的 时间占比
final int ioRatio = this.ioRatio;
boolean ranTasks;
// 条件成立: 处理IO事件(就绪事件) 时间占比为 100%
if (ioRatio == 100) {
try {
// 条件成立: 有IO事件(就绪事件)
if (strategy > 0) {
// 处理IO事件(就绪事件) 的核心方法
processSelectedKeys();
}
} finally {
// 处理 本地任务 的核心方法
ranTasks = runAllTasks();
}
// 条件成立: IO时间占比不是100% 且 有IO事件(就绪事件)
} else if (strategy > 0) {
// 获取系统当前时间
final long ioStartTime = System.nanoTime();
try {
// 处理IO事件(就绪事件)
processSelectedKeys();
} finally {
// 计算 执行IO事件(就绪事件) 耗费了 多长时间
final long ioTime = System.nanoTime() - ioStartTime;
// 根据IO的执行时间,计算出本地任务需要执行多长时间
// 按照规定时间,执行本地任务
ranTasks = runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
// 条件成立: IO时间占比不是100% 且 没有IO事件(就绪事件)
} else {
// 处理最多64个本地任务
ranTasks = runAllTasks(0);
}
// 条件成立: 本地任务执行成功 或者 IO就绪事件个数>0
if (ranTasks || strategy > 0) {
if (selectCnt > MIN_PREMATURE_SELECTOR_RETURNS && logger.isDebugEnabled()) {
logger.debug("Selector.select() returned prematurely {} times in a row for Selector {}.",
selectCnt - 1, selector);
}
// 将 selectCnt 置为0
selectCnt = 0;
}
// 来到这说明: 本次轮询 既没有 执行本地任务 也没有 IO就绪事件
// 处理 epoll Bug
else if (unexpectedSelectorWakeup(selectCnt)) { // Unexpected wakeup (unusual case)
selectCnt = 0;
}
}
部分二中的代码, 实际上也很好理解, 我们首先要清楚 NioEventLoop 线程主要处理 的两件事 :
1. **IO 事件 (selector的就绪事件)** **processSelectedKeys()**
2. **本地任务 (分为 本地普通任务 和 本地调度任务) ** **runAllTasks()**
另外需要 注意 变量 ioRatio: 该变量主要表示 执行IO事件 在总处理时间(IO事件 + 本地任务)上的占比。
processSelectedKeys() 和 runAllTasks() 的代码分析,下面会做详细分析。
注意处理本地任务的方法 在上述代码中有三种:
1. runAllTasks();
2. runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
3. runAllTasks(0);
4. 处理IO事件
private void processSelectedKeysOptimized() {
// 遍历就绪事件集合
for (int i = 0; i < selectedKeys.size; ++i) {
// SelectionKey 表示就绪事件
final SelectionKey k = selectedKeys.keys[i];
selectedKeys.keys[i] = null;
// 拿到就绪事件上的附件 这里会拿到注册阶段,咱们向Selector提供的Channel对象
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
// 处理IO事件...
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
if (needsToSelectAgain) {
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
总结上述代码中的 操作如下:
1. 遍历 **selectedKeys** 事件就绪集合 ,拿到一个个**SelectionKey**
2. 通过获取 **SelectionKey** 中的 **attchment** 可以拿到 与该就绪事件绑定的 **Channel**
3. 调用 `processSelectedKey(k, (AbstractNioChannel) a);` 按照不同的事件做对应的处理
针对不同的就绪事件,做不同的处理操作:
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
// 1. NioServerSocketChannel -> NioMessageUnsafe
// 2. NioSocketChannel -> NioByteUnsafe
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
if (!k.isValid()) {
final EventLoop eventLoop;
try {
eventLoop = ch.eventLoop();
} catch (Throwable ignored) {
return;
}
if (eventLoop == this) {
unsafe.close(unsafe.voidPromise());
}
return;
}
try {
// 获取就绪事件
int readyOps = k.readyOps();
// 1.就绪事件为: OP_CONNECT
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
unsafe.finishConnect();
}
// 2.就绪事件为:OP_WRITE
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
ch.unsafe().forceFlush();
}
// 3.就绪事件为:OP_READ 或者 OP_ACCEPT
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
// 这里针对 客户端 或者 服务端的Channel 做不同的 read操作
// 1. 服务端Channel accept 事件
// 2. 客户端Channel read 事件
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
上述代码很简单,实际上类似于 我们最基本的 NIO 编程案例, 根据不同的 就绪事件 做不同的操作。
最终处理的方法将会根据 Channel 的不同,调用其内部类 unsafe中的方法来处理。
具体不同的 Channel 中 unsafe 处理的细节源码 会在下一篇文章《服务端/客户端 Channel消息处理源码分析》中做详细介绍。
5. 处理本地任务
在核心逻辑的第二部分中,强调了处理本地任务的方法 有三种:
1. **runAllTasks();**
2. **runAllTasks(ioTime * (100 - ioRatio) / ioRatio);**
3. **runAllTasks(0);**
实际上 是两个 重载方法, 第2 和 第3个是属于同一种方法
- runAllTasks() 空参的
- runAllTasks(long timeoutNanos) 带有时间参数的
首先来看 空参的runAllTasks() :
// 返回: 是否执行了任务
protected boolean runAllTasks() {
assert inEventLoop();
boolean fetchedAll;
boolean ranAtLeastOne = false;
do {
// fetchFromScheduledTaskQueue() 将 调度任务队列中 需要被调度的任务 转移到普通任务队列taskQueue内
// fetchedAll表示 需要被调度的任务 有没有 转移完
fetchedAll = fetchFromScheduledTaskQueue();
// 条件成立: 执行了任务
if (runAllTasksFrom(taskQueue)) {
ranAtLeastOne = true;
}
} while (!fetchedAll); // keep on processing until we fetched all scheduled tasks.
// 执行到这 需要调度的任务和普通任务全部都执行完了
if (ranAtLeastOne) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
afterRunningAllTasks();
return ranAtLeastOne;
}
上面代码 没什么好说的,主要做的目的就是, 将 本地调度任务队列中的可调度任务 转移到 本地普通任务队列中 去执行。
再来看 有参的 runAllTasks(long timeoutNanos) :
// 参数: 表示执行任务最多可用的时长
protected boolean runAllTasks(long timeoutNanos) {
// 转移 需要被调度任务到 普通任务队列
fetchFromScheduledTaskQueue();
Runnable task = pollTask();
if (task == null) {
afterRunningAllTasks();
return false;
}
// deadline 表示执行任务的截止时间
final long deadline = timeoutNanos > 0 ? ScheduledFutureTask.nanoTime() + timeoutNanos : 0;
// 表示已经执行的任务个数
long runTasks = 0;
// 最后一个任务的执行时间戳
long lastExecutionTime;
for (;;) {
// 执行任务
safeExecute(task);
runTasks ++;
// 0x3f=> 十进制63 二进制111111
//64 => 1000000 & 000000 = 0
//128 => 10000000 & 000000 = 0
//192 => 11000000 & 000000 = 0
// 结论: 每执行64个任务 这个条件会成立一次,
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;
}
该 runAllTasks(long timeoutNanos)方法与 上面空参的 runAllTasks() 不同的点如下:
1. **将可调度任务 转移到 本地普通任务队列中的 操作 只做一次**
2. **if ((runTasks & 0x3F) == 0)** 每执行64个任务,该条件就会成立一次, 来判断 执行任务的时间是否超过了**timeoutNanos 超时时间**
1. 超过了 则立即退出
2. 没超过 则继续执行任务,直到再次执行了 64个任务,又会进入该判断。
6.总结
至此 NioEventLoop 运行相关的代码分析完毕, 本篇文章需要弄清楚以下几点:
- NioEventLoop 线程 需要处理的 事情有:
- IO事件 (selector就绪事件)
- 本地任务 (普通任务, 调度任务)
- 处理IO事件 总是优先于 本地任务, 具体的执行时间配比 按照 ioRatio 字段来决定。
- 处理IO事件时,会根据 就绪事件的不同 和 Channel 的不同 来做具体的操作。
- 处理本地任务时
- 会先将 可调度任务转移到本地普通任务队列中
- 逐个处理 本地普通任务队列 (包括 普通任务 和 可调度任务) 中的任务.
具体 服务端 / 客户端 Channel 如何 处理 IO事件 的源码,会在下一篇文章中详细分析。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)