相关文章链接
processSelectedKeys() vs runAllTasks()
NioServerSocketChannel-Unsafe初始化详解
NioEventLoop 的 run() 方法详解
一、从线程启动说起
我们在前面讲 Channel 注册的时候提到,当第一次调用 eventLoop.execute(task) 时,会触发线程的创建和启动。我们来回顾一下这个过程:
// SingleThreadEventExecutor.java
private void doStartThread() {
executor.execute(new Runnable() {
@Override
public void run() {
// 保存线程引用
thread = Thread.currentThread();
// 执行 NioEventLoop 的 run() 方法
// 这个 run() 方法会一直循环,处理 I/O 事件和任务
SingleThreadEventExecutor.this.run();
}
});
}
这里的 SingleThreadEventExecutor.this.run() 是一个抽象方法,实际实现在 NioEventLoop 中。线程启动后,就会一直执行这个 run() 方法,直到 EventLoop 关闭。
二、run() 方法的整体结构
我们来看 NioEventLoop 的 run() 方法:
// NioEventLoop.java
@Override
protected void run() {
for (;;) { // 无限循环,直到 EventLoop 关闭
try {
// 步骤1:根据策略选择是阻塞 select 还是非阻塞 selectNow
switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
case SelectStrategy.CONTINUE:
continue;
case SelectStrategy.SELECT:
// 阻塞等待 I/O 事件
select(wakenUp.getAndSet(false));
if (wakenUp.get()) {
selector.wakeup();
}
default:
}
cancelledKeys = 0;
needsToSelectAgain = false;
// ioRatio 控制 I/O 时间和任务时间的比例,默认是 50
final int ioRatio = this.ioRatio;
if (ioRatio == 100) {
// 如果 ioRatio 是 100,先处理完所有 I/O 事件,再处理所有任务
try {
// 步骤2:处理 I/O 事件
processSelectedKeys();
} finally {
// 步骤3:处理任务队列,不限制时间
runAllTasks();
}
} else {
// 如果 ioRatio 不是 100,需要按比例分配时间
final long ioStartTime = System.nanoTime();
try {
// 步骤2:处理 I/O 事件
processSelectedKeys();
} finally {
// 计算处理 I/O 事件花费的时间
final long ioTime = System.nanoTime() - ioStartTime;
// 步骤3:处理任务队列,限制时间
// 如果 ioRatio 是 50,那么任务时间也应该是 ioTime
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
}
} catch (Throwable t) {
handleLoopException(t);
}
// 检查是否需要关闭
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return; // 退出循环,线程结束
}
}
} catch (Throwable t) {
handleLoopException(t);
}
}
}
整个 run() 方法就是一个无限循环,不断重复三个步骤:
- select() - 等待 I/O 事件
- processSelectedKeys() - 处理 I/O 事件
- runAllTasks() - 处理任务队列
这就是 Netty 的 Reactor 模型的核心实现。
三、select() - 等待 I/O 事件
3.1 SelectStrategy 的作用
在进入 select() 之前,会先调用 selectStrategy.calculateStrategy() 来决定是阻塞等待还是非阻塞检查:
// DefaultSelectStrategy.java
@Override
public int calculateStrategy(IntSupplier selectSupplier, boolean hasTasks) throws Exception {
// 如果有任务,执行非阻塞的 selectNow()
// 如果没有任务,返回 SELECT 标记,后续会执行阻塞的 select()
return hasTasks ? selectSupplier.get() : SelectStrategy.SELECT;
}
这里的逻辑很简单:
- 如果任务队列中有任务,就调用
selectNow(),非阻塞地检查一下是否有 I/O 事件,然后立即返回去处理任务 - 如果任务队列中没有任务,就返回 SELECT,后续会调用阻塞的
select(),等待 I/O 事件
这个设计保证了任务能够及时被处理,不会因为阻塞在 select() 上而延迟。
3.2 select() 方法的实现
我们来看 select() 方法的实现:
// NioEventLoop.java
private void select(boolean oldWakenUp) throws IOException {
Selector selector = this.selector;
try {
int selectCnt = 0; // select 调用次数计数器
long currentTimeNanos = System.nanoTime();
// 计算截止时间
// delayNanos() 返回最近的定时任务还有多久执行
long selectDeadLineNanos = currentTimeNanos + delayNanos(currentTimeNanos);
for (;;) {
// 计算超时时间(毫秒)
long timeoutMillis = (selectDeadLineNanos - currentTimeNanos + 500000L) / 1000000L;
// 如果超时时间 <= 0,说明有定时任务要执行了
if (timeoutMillis <= 0) {
if (selectCnt == 0) {
// 如果还没有 select 过,先非阻塞地 select 一次
selector.selectNow();
selectCnt = 1;
}
break; // 退出循环
}
// 如果任务队列中有任务,并且成功设置了 wakenUp 标志
if (hasTasks() && wakenUp.compareAndSet(false, true)) {
// 非阻塞地 select 一次,然后退出
selector.selectNow();
selectCnt = 1;
break;
}
// 阻塞等待 I/O 事件,最多等待 timeoutMillis 毫秒
int selectedKeys = selector.select(timeoutMillis);
selectCnt++;
// 如果有 I/O 事件、或者被唤醒、或者有任务、或者有定时任务
// 就退出循环
if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || hasTasks() || hasScheduledTasks()) {
break;
}
// 如果线程被中断,退出循环
if (Thread.interrupted()) {
selectCnt = 1;
break;
}
long time = System.nanoTime();
// 检查是否触发了 JDK 的 epoll bug
// 如果 select() 立即返回(没有阻塞),并且没有 I/O 事件
// 说明可能触发了 epoll bug
if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
// 正常情况,重置计数器
selectCnt = 1;
} else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
// 如果连续多次立即返回,说明触发了 epoll bug
// 重建 Selector
rebuildSelector();
selector = this.selector;
selector.selectNow();
selectCnt = 1;
break;
}
currentTimeNanos = time;
}
} catch (CancelledKeyException e) {
// 忽略
}
}
这个方法看起来很复杂,但核心逻辑很清晰:
- 计算超时时间:根据最近的定时任务计算出超时时间
- 检查是否需要立即返回:如果有定时任务要执行、或者有普通任务,就立即返回
- 阻塞等待:调用
selector.select(timeoutMillis)阻塞等待 I/O 事件 - 检查退出条件:如果有 I/O 事件、或者被唤醒、或者有任务,就退出循环
- 处理 epoll bug:如果连续多次立即返回,说明触发了 JDK 的 epoll bug,需要重建 Selector
3.3 wakenUp 机制
wakenUp 是一个 AtomicBoolean,用于唤醒阻塞在 select() 上的线程。当外部线程向 EventLoop 提交任务时,会调用 wakeup() 方法:
// SingleThreadEventExecutor.java
protected void wakeup(boolean inEventLoop) {
if (!inEventLoop && wakenUp.compareAndSet(false, true)) {
selector.wakeup(); // 唤醒阻塞在 select() 上的线程
}
}
这个机制保证了任务能够及时被处理。如果没有这个机制,EventLoop 可能会一直阻塞在 select() 上,导致任务无法及时执行。
3.4 JDK 的 epoll bug
JDK 的 NIO 有一个臭名昭著的 bug:在 Linux 系统上,即使没有 I/O 事件,selector.select() 也可能立即返回,导致 CPU 100%。
Netty 的解决方案是:如果检测到连续多次(默认 512 次)立即返回,就认为触发了 epoll bug,然后重建 Selector:
private void rebuildSelector() {
final Selector oldSelector = selector;
final Selector newSelector;
// 创建新的 Selector
newSelector = openSelector();
// 将所有 Channel 从旧 Selector 转移到新 Selector
int nChannels = 0;
for (SelectionKey key: oldSelector.keys()) {
Object a = key.attachment();
try {
if (!key.isValid() || key.channel().keyFor(newSelector) != null) {
continue;
}
int interestOps = key.interestOps();
key.cancel();
// 将 Channel 注册到新 Selector
SelectionKey newKey = key.channel().register(newSelector, interestOps, a);
if (a instanceof AbstractNioChannel) {
((AbstractNioChannel) a).selectionKey = newKey;
}
nChannels++;
} catch (Exception e) {
// 异常处理
}
}
// 替换 Selector
selector = newSelector;
// 关闭旧 Selector
try {
oldSelector.close();
} catch (Throwable t) {
// 忽略
}
}
这个方法会创建一个新的 Selector,然后把所有 Channel 从旧 Selector 转移到新 Selector,最后关闭旧 Selector。这样就解决了 epoll bug 的问题。
四、processSelectedKeys() - 处理 I/O 事件
select() 返回后,如果有 I/O 事件就绪,就会调用 processSelectedKeys() 来处理这些事件。
4.1 优化的 SelectedSelectionKeySet
Netty 对 Selector 的 selectedKeys 做了优化,使用了自己实现的 SelectedSelectionKeySet 来替代 JDK 的 HashSet:
// NioEventLoop.java
private void processSelectedKeys() {
if (selectedKeys != null) {
// 使用优化的 SelectedSelectionKeySet
processSelectedKeysOptimized();
} else {
// 使用 JDK 原生的 Set
processSelectedKeysPlain(selector.selectedKeys());
}
}
SelectedSelectionKeySet 是一个基于数组的实现,比 HashSet 更快:
// SelectedSelectionKeySet.java
final class SelectedSelectionKeySet extends AbstractSet<SelectionKey> {
SelectionKey[] keys;
int size;
SelectedSelectionKeySet() {
keys = new SelectionKey[1024];
}
@Override
public boolean add(SelectionKey o) {
if (o == null) {
return false;
}
keys[size++] = o;
// 如果数组满了,扩容
if (size == keys.length) {
increaseCapacity();
}
return true;
}
private void increaseCapacity() {
SelectionKey[] newKeys = new SelectionKey[keys.length << 1];
System.arraycopy(keys, 0, newKeys, 0, size);
keys = newKeys;
}
void reset() {
reset(0);
}
void reset(int start) {
Arrays.fill(keys, start, size, null);
size = 0;
}
}
这个实现很简单,就是一个动态数组,add 操作是 O(1),比 HashSet 的 O(1) 更快(因为没有哈希计算)。
4.2 processSelectedKeysOptimized() 的实现
我们来看优化版本的实现:
// NioEventLoop.java
private void processSelectedKeysOptimized() {
// 遍历所有就绪的 SelectionKey
for (int i = 0; i < selectedKeys.size; ++i) {
final SelectionKey k = selectedKeys.keys[i];
// 处理完后,将数组元素置为 null,帮助 GC
selectedKeys.keys[i] = null;
// 获取 attachment,也就是 NioChannel
final Object a = k.attachment();
if (a instanceof AbstractNioChannel) {
// 处理这个 Channel 的事件
processSelectedKey(k, (AbstractNioChannel) a);
} else {
@SuppressWarnings("unchecked")
NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;
processSelectedKey(k, task);
}
// 检查是否需要再次 select
if (needsToSelectAgain) {
selectedKeys.reset(i + 1);
selectAgain();
i = -1;
}
}
}
这里遍历所有就绪的 SelectionKey,然后调用 processSelectedKey() 来处理每个 Channel 的事件。
4.3 processSelectedKey() - 处理单个 Channel 的事件
这是处理 I/O 事件的核心方法:
// NioEventLoop.java
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
// 获取 Channel 的 Unsafe 对象
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe();
// 检查 SelectionKey 是否有效
if (!k.isValid()) {
// SelectionKey 无效,关闭 Channel
unsafe.close(unsafe.voidPromise());
return;
}
try {
// 获取就绪的操作类型
int readyOps = k.readyOps();
// 处理 OP_CONNECT 事件(客户端连接完成)
if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
int ops = k.interestOps();
// 移除 OP_CONNECT,避免重复触发
ops &= ~SelectionKey.OP_CONNECT;
k.interestOps(ops);
// 调用 finishConnect()
unsafe.finishConnect();
}
// 处理 OP_WRITE 事件(可写)
if ((readyOps & SelectionKey.OP_WRITE) != 0) {
// 调用 forceFlush(),将缓冲区的数据写出去
ch.unsafe().forceFlush();
}
// 处理 OP_READ 或 OP_ACCEPT 事件
// 这是最常见的事件
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
// 调用 read() 方法
// 对于 ServerSocketChannel,这会接受新连接
// 对于 SocketChannel,这会读取数据
unsafe.read();
}
} catch (CancelledKeyException ignored) {
unsafe.close(unsafe.voidPromise());
}
}
这个方法根据不同的事件类型,调用不同的处理方法:
- OP_CONNECT:客户端连接完成,调用
finishConnect() - OP_WRITE:可写,调用
forceFlush()将缓冲区的数据写出去 - OP_READ / OP_ACCEPT:可读或有新连接,调用
read()
4.4 unsafe.read() 触发入站事件
我们重点看 unsafe.read() 这个方法,因为它会触发入站事件的传播。我们在前面讲入站事件处理的时候已经详细说过这个流程了,这里简单回顾一下:
// AbstractNioMessageChannel.NioMessageUnsafe.java
@Override
public void read() {
final ChannelPipeline pipeline = pipeline();
try {
do {
// 接受新连接或读取数据
int localRead = doReadMessages(readBuf);
if (localRead == 0) {
break;
}
if (localRead < 0) {
closed = true;
break;
}
} while (allocHandle.continueReading());
} catch (Throwable t) {
exception = t;
}
int size = readBuf.size();
for (int i = 0; i < size; i ++) {
readPending = false;
// 触发 channelRead 事件
// 这里会从 head 开始,沿着 Pipeline 传播
pipeline.fireChannelRead(readBuf.get(i));
}
readBuf.clear();
allocHandle.readComplete();
// 触发 channelReadComplete 事件
pipeline.fireChannelReadComplete();
if (exception != null) {
closed = closeOnReadError(exception);
pipeline.fireExceptionCaught(exception);
}
}
可以看到,unsafe.read() 会调用 pipeline.fireChannelRead(),触发入站事件的传播。这就和我们前面讲的入站事件处理流程对应上了。
整个流程是这样的:
NioEventLoop.run()
↓
select() - 等待 I/O 事件
↓
processSelectedKeys() - 处理就绪的事件
↓
processSelectedKey() - 处理单个 Channel 的事件
↓
unsafe.read() - 读取数据
↓
pipeline.fireChannelRead() - 触发入站事件
↓
head.channelRead() → handler1.channelRead() → ... → tail.channelRead()
这就是从 EventLoop 的事件循环到 Pipeline 的事件传播的完整流程。
五、runAllTasks() - 处理任务队列
处理完 I/O 事件后,EventLoop 会处理任务队列中的任务。这些任务可能是用户提交的,也可能是 Netty 内部提交的。
5.1 任务队列的设计
NioEventLoop 有两个任务队列:
- taskQueue:普通任务队列,存储通过
execute()提交的任务 - scheduledTaskQueue:定时任务队列,存储通过
schedule()提交的定时任务
// SingleThreadEventExecutor.java
private final Queue<Runnable> taskQueue; // 普通任务队列
// AbstractScheduledEventExecutor.java
PriorityQueue<ScheduledFutureTask<?>> scheduledTaskQueue; // 定时任务队列
5.2 向 EventLoop 提交任务
我们可以通过 execute() 方法向 EventLoop 提交任务:
eventLoop.execute(new Runnable() {
@Override
public void run() {
// 这个任务会在 EventLoop 线程中执行
System.out.println("Task executed in EventLoop");
}
});
execute() 方法的实现:
// SingleThreadEventExecutor.java
@Override
public void execute(Runnable task) {
if (task == null) {
throw new NullPointerException("task");
}
boolean inEventLoop = inEventLoop();
// 将任务添加到队列
addTask(task);
if (!inEventLoop) {
// 如果当前线程不是 EventLoop 线程,启动 EventLoop 线程
startThread();
if (isShutdown()) {
// 如果 EventLoop 已经关闭,移除任务并拒绝
boolean reject = false;
try {
if (removeTask(task)) {
reject = true;
}
} catch (UnsupportedOperationException e) {
// 队列不支持移除操作
}
if (reject) {
reject();
}
}
}
if (!addTaskWakesUp && wakesUpForTask(task)) {
// 唤醒阻塞在 select() 上的线程
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);
}
可以看到,execute() 方法会将任务添加到 taskQueue,然后唤醒 EventLoop 线程(如果需要的话)。
5.3 runAllTasks() 的实现
我们来看 runAllTasks() 方法的实现:
// SingleThreadEventExecutor.java
protected boolean runAllTasks() {
assert inEventLoop();
boolean fetchedAll;
boolean ranAtLeastOne = false;
do {
// 从定时任务队列中取出到期的任务,放到普通任务队列
fetchedAll = fetchFromScheduledTaskQueue();
// 执行普通任务队列中的所有任务
if (runAllTasksFrom(taskQueue)) {
ranAtLeastOne = true;
}
} while (!fetchedAll); // 如果还有定时任务到期,继续循环
if (ranAtLeastOne) {
lastExecutionTime = ScheduledFutureTask.nanoTime();
}
// 执行收尾任务
afterRunningAllTasks();
return ranAtLeastOne;
}
这个方法的逻辑是:
- 从定时任务队列中取出到期的任务,放到普通任务队列
- 执行普通任务队列中的所有任务
- 重复上述步骤,直到没有到期的定时任务
- 执行收尾任务
5.4 带超时的 runAllTasks()
在 run() 方法中,如果 ioRatio 不是 100,会调用带超时的 runAllTasks():
// SingleThreadEventExecutor.java
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++;
// 每执行 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;
}
这个方法会限制任务执行的时间,避免任务执行时间过长,导致 I/O 事件得不到及时处理。
注意这里有个优化:不是每执行一个任务就检查一次时间,而是每执行 64 个任务才检查一次。这是因为 System.nanoTime() 是一个系统调用,比较耗时,所以不能频繁调用。
5.5 fetchFromScheduledTaskQueue() - 取出到期的定时任务
// SingleThreadEventExecutor.java
private boolean fetchFromScheduledTaskQueue() {
long nanoTime = AbstractScheduledEventExecutor.nanoTime();
// 从定时任务队列中取出到期的任务
Runnable scheduledTask = pollScheduledTask(nanoTime);
while (scheduledTask != null) {
// 将定时任务添加到普通任务队列
if (!taskQueue.offer(scheduledTask)) {
// 队列满了,放回定时任务队列
scheduledTaskQueue().add((ScheduledFutureTask<?>) scheduledTask);
return false;
}
// 继续取出下一个到期的定时任务
scheduledTask = pollScheduledTask(nanoTime);
}
return true;
}
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;
}
这个方法会从定时任务队列(优先队列)中取出所有到期的任务,放到普通任务队列中。
5.6 safeExecute() - 安全地执行任务
// SingleThreadEventExecutor.java
protected static void safeExecute(Runnable task) {
try {
task.run();
} catch (Throwable t) {
logger.warn("A task raised an exception. Task: {}", task, t);
}
}
这个方法会捕获任务执行过程中的所有异常,避免一个任务的异常导致整个 EventLoop 崩溃。
六、ioRatio 的作用
ioRatio 用于控制 I/O 处理和任务处理的时间比例,默认值是 50,表示 I/O 时间和任务时间各占 50%。
if (ioRatio == 100) {
// 先处理完所有 I/O 事件,再处理所有任务
processSelectedKeys();
runAllTasks();
} else {
// 按比例分配时间
final long ioStartTime = System.nanoTime();
processSelectedKeys();
final long ioTime = System.nanoTime() - ioStartTime;
// 如果 ioRatio 是 50,那么任务时间也应该是 ioTime
// 如果 ioRatio 是 70,那么任务时间应该是 ioTime * 30 / 70
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
举个例子:
- 如果 ioRatio = 50,I/O 处理花费了 100ms,那么任务处理最多也只能花费 100ms
- 如果 ioRatio = 70,I/O 处理花费了 100ms,那么任务处理最多只能花费 43ms(100 * 30 / 70)
- 如果 ioRatio = 30,I/O 处理花费了 100ms,那么任务处理最多可以花费 233ms(100 * 70 / 30)
这个设计保证了 I/O 事件和任务都能得到及时处理,不会因为任务太多而导致 I/O 事件得不到处理,也不会因为 I/O 事件太多而导致任务得不到处理。
一般情况下,默认的 50 就够用了。如果你的应用 I/O 密集,可以调高 ioRatio;如果你的应用任务密集,可以调低 ioRatio。
七、完整的事件处理流程图
现在我们把整个流程串起来,看看从 EventLoop 启动到事件处理的完整流程:
1. Channel 注册到 EventLoop
↓
2. 第一次调用 execute(),触发线程创建
↓
3. ThreadPerTaskExecutor 创建线程
↓
4. 线程执行 NioEventLoop.run()
↓
5. 进入无限循环
├─→ select() - 等待 I/O 事件
│ ├─ 检查任务队列,决定是阻塞还是非阻塞
│ ├─ 计算超时时间(根据定时任务)
│ ├─ 调用 selector.select(timeout) 阻塞等待
│ ├─ 检查是否触发 epoll bug
│ └─ 如果触发,重建 Selector
│
├─→ processSelectedKeys() - 处理 I/O 事件
│ ├─ 遍历所有就绪的 SelectionKey
│ ├─ 调用 processSelectedKey() 处理单个 Channel
│ │ ├─ OP_CONNECT → finishConnect()
│ │ ├─ OP_WRITE → forceFlush()
│ │ └─ OP_READ/OP_ACCEPT → unsafe.read()
│ │ ├─ doReadMessages() 读取数据
│ │ ├─ pipeline.fireChannelRead() 触发入站事件
│ │ │ └─ head → handler1 → handler2 → ... → tail
│ │ └─ pipeline.fireChannelReadComplete()
│ └─ 返回
│
├─→ runAllTasks() - 处理任务队列
│ ├─ fetchFromScheduledTaskQueue() 取出到期的定时任务
│ ├─ 执行普通任务队列中的任务
│ │ ├─ 每执行 64 个任务检查一次超时
│ │ └─ 如果超时,退出循环
│ └─ afterRunningAllTasks() 执行收尾任务
│
└─→ 检查是否需要关闭
├─ 如果需要关闭,执行关闭流程
└─ 否则,继续循环
八、EventLoop 的关闭流程
当我们调用 eventLoopGroup.shutdownGracefully() 时,会触发 EventLoop 的关闭流程:
// SingleThreadEventExecutor.java
@Override
public Future<?> shutdownGracefully(long quietPeriod, long timeout, TimeUnit unit) {
// 设置状态为 ST_SHUTTING_DOWN
if (isShuttingDown()) {
return terminationFuture();
}
boolean inEventLoop = inEventLoop();
boolean wakeup;
int oldState;
for (;;) {
if (isShuttingDown()) {
return terminationFuture();
}
int newState;
wakeup = true;
oldState = state;
if (inEventLoop) {
newState = ST_SHUTTING_DOWN;
} else {
switch (oldState) {
case ST_NOT_STARTED:
case ST_STARTED:
newState = ST_SHUTTING_DOWN;
break;
default:
newState = oldState;
wakeup = false;
}
}
if (STATE_UPDATER.compareAndSet(this, oldState, newState)) {
break;
}
}
gracefulShutdownQuietPeriod = unit.toNanos(quietPeriod);
gracefulShutdownTimeout = unit.toNanos(timeout);
if (oldState == ST_NOT_STARTED) {
scheduleExecution();
}
if (wakeup) {
wakeup(inEventLoop);
}
return terminationFuture();
}
在 run() 方法的最后,会检查是否需要关闭:
try {
if (isShuttingDown()) {
closeAll();
if (confirmShutdown()) {
return; // 退出循环,线程结束
}
}
} catch (Throwable t) {
handleLoopException(t);
}
confirmShutdown() 方法会确认是否可以关闭:
// SingleThreadEventExecutor.java
protected boolean confirmShutdown() {
if (!isShuttingDown()) {
return false;
}
if (!inEventLoop()) {
throw new IllegalStateException("must be invoked from an event loop");
}
cancelScheduledTasks();
if (gracefulShutdownStartTime == 0) {
gracefulShutdownStartTime = ScheduledFutureTask.nanoTime();
}
// 执行所有任务
if (runAllTasks() || runShutdownHooks()) {
if (isShutdown()) {
return true;
}
// 如果还有任务,重置开始时间
if (gracefulShutdownQuietPeriod == 0) {
return true;
}
wakeup(true);
return false;
}
final long nanoTime = ScheduledFutureTask.nanoTime();
// 检查是否超时
if (isShutdown() || nanoTime - gracefulShutdownStartTime > gracefulShutdownTimeout) {
return true;
}
// 检查是否在静默期内
if (nanoTime - lastExecutionTime <= gracefulShutdownQuietPeriod) {
wakeup(true);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Ignore
}
return false;
}
return true;
}
这个方法会等待所有任务执行完成,然后才真正关闭 EventLoop。这就是"优雅关闭"的含义。
九、总结
NioEventLoop 的 run() 方法是 Netty 的核心,它实现了 Reactor 模型的事件循环。整个流程可以总结为:
-
select() - 等待 I/O 事件
- 根据任务队列和定时任务决定是阻塞还是非阻塞
- 处理 JDK 的 epoll bug
- 通过 wakenUp 机制及时唤醒
-
processSelectedKeys() - 处理 I/O 事件
- 使用优化的 SelectedSelectionKeySet 提高性能
- 根据不同的事件类型调用不同的处理方法
- 通过 unsafe.read() 触发 Pipeline 的入站事件传播
-
runAllTasks() - 处理任务队列
- 先处理到期的定时任务
- 再处理普通任务
- 通过 ioRatio 控制时间分配
- 每 64 个任务检查一次超时
这三个步骤不断循环,直到 EventLoop 关闭。这就是 Netty 如何用单线程处理多个 Channel 的秘密。
整个设计非常巧妙:
- 通过 Selector 实现了 I/O 多路复用,一个线程可以监听多个 Channel
- 通过任务队列实现了异步任务的执行
- 通过 ioRatio 实现了 I/O 和任务的时间平衡
- 通过优雅关闭保证了所有任务都能执行完成
7206

被折叠的 条评论
为什么被折叠?



