Netty源码分析---NioEventLoop的run方法详解

相关文章链接

位运算详解

waken方法详解

ThreadPerTaskExecutor与线程创建详解

processSelectedKeys() vs runAllTasks()

NioServerSocketChannel-Unsafe初始化详解

NioEventLoop的run方法详解

NioEventLoopGroup深度解析

inEventLoop方法详解

executionMask详解

Netty源码分析–认真系列(一)

Netty源码分析–认真系列(二)

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() 方法就是一个无限循环,不断重复三个步骤:

  1. select() - 等待 I/O 事件
  2. processSelectedKeys() - 处理 I/O 事件
  3. 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) {
        // 忽略
    }
}

这个方法看起来很复杂,但核心逻辑很清晰:

  1. 计算超时时间:根据最近的定时任务计算出超时时间
  2. 检查是否需要立即返回:如果有定时任务要执行、或者有普通任务,就立即返回
  3. 阻塞等待:调用 selector.select(timeoutMillis) 阻塞等待 I/O 事件
  4. 检查退出条件:如果有 I/O 事件、或者被唤醒、或者有任务,就退出循环
  5. 处理 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());
    }
}

这个方法根据不同的事件类型,调用不同的处理方法:

  1. OP_CONNECT:客户端连接完成,调用 finishConnect()
  2. OP_WRITE:可写,调用 forceFlush() 将缓冲区的数据写出去
  3. 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 有两个任务队列:

  1. taskQueue:普通任务队列,存储通过 execute() 提交的任务
  2. 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;
}

这个方法的逻辑是:

  1. 从定时任务队列中取出到期的任务,放到普通任务队列
  2. 执行普通任务队列中的所有任务
  3. 重复上述步骤,直到没有到期的定时任务
  4. 执行收尾任务

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 模型的事件循环。整个流程可以总结为:

  1. select() - 等待 I/O 事件

    • 根据任务队列和定时任务决定是阻塞还是非阻塞
    • 处理 JDK 的 epoll bug
    • 通过 wakenUp 机制及时唤醒
  2. processSelectedKeys() - 处理 I/O 事件

    • 使用优化的 SelectedSelectionKeySet 提高性能
    • 根据不同的事件类型调用不同的处理方法
    • 通过 unsafe.read() 触发 Pipeline 的入站事件传播
  3. runAllTasks() - 处理任务队列

    • 先处理到期的定时任务
    • 再处理普通任务
    • 通过 ioRatio 控制时间分配
    • 每 64 个任务检查一次超时

这三个步骤不断循环,直到 EventLoop 关闭。这就是 Netty 如何用单线程处理多个 Channel 的秘密。

整个设计非常巧妙:

  • 通过 Selector 实现了 I/O 多路复用,一个线程可以监听多个 Channel
  • 通过任务队列实现了异步任务的执行
  • 通过 ioRatio 实现了 I/O 和任务的时间平衡
  • 通过优雅关闭保证了所有任务都能执行完成
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值