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

Netty源码分析:NioEventLoop启动以及其IO操作和Task任务的处理 Netty源码分析:NioEventLoop类的execute、startThread、run方法在上篇博文分析服务端启动的过程中,我们遇到了如下的代码片段, if (eventLoop.inEventLoop()) {//判断当前线程是否为该EventLoop中拥有的线程,如果是,则直接注册,如果不是,则添加一个任务到该线程中 register 阅读详情

相关文章链接

位运算详解

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 和任务的时间平衡
  • 通过优雅关闭保证了所有任务都能执行完成
Netty源码分析系列之NioEventLoop的执行流程 NioEventLoop作为一个NIO事件轮询器,到底在循环处理什么呢?JDK中臭名昭著的空轮询BUG,在netty中又是如何避免的呢? 阅读详情

相关推荐

netty源码分析6-NioEventLoop启动和run方法

分享内容如下 select启动跟踪 定时任务机制分析 NioEventLoopIO轮询方法分析 select启动跟踪 NioEventLoop中有循环select实现,是什么时候被调用的? 循环select由run方法实现,调用链如下图 6 ServerBootstrap(AbstractBootstrap<B,C>).initAndRegister() line...

chenchangqun11的专栏 384

Netty Reactor 模型:Boss、Worker 与 EventLoop

基于源码深度解析,Netty Reactor 模型:Boss、Worker 与 EventLoop

m0_57877510的博客 479

信号完整性之阻抗(五)

一、什么是阻抗 阻抗的概念是从高中或者初中大家可能就会接触到,没想它一直陪伴我十年了。在我的印象中,提到阻抗我可能就会想到电阻,这样的思想局限也限制了我平常思考问题。所以今天也想把阻抗这个概念在顺理一遍,有错误或者欠缺的地方希望大家指正。 1.什么是阻抗: ==在具有电阻、电感和电容的电路里,对电路中的电流所起的阻碍作用叫做阻抗。阻抗常用Z表示,是一个复数,实部称为电阻,虚部称为电抗,其中电容在电路中对交流电所起的阻碍作用称为容抗 ,电感在电路中对交流电所起的阻碍作用称为感抗,电容和电感在电路中对交流电引

刘小同学的博客 2587

Netty中NioEventLoop源码分析

版本 本次源码分析基于Netty的版本为4.1 源码分析 NioEventLoop可以视为java中的一个线程,只不过NioEventLoop处理的事件,以及内部的处理逻辑会有所不同。先看一下类的继承关系: 可以看到NioEventLoop实现了很多接口,特别是EventLoop和ScheduledExecutorService,所以NioEventLoop不仅能实现普通的task,还能实现定时t...

Forest441的博客 1043

DELL SC存储扩容避坑指南:6Gb vs 12Gb扩展柜混插的真相

本文深入解析DELL SC存储扩容中6Gb与12Gb扩展柜混插的核心规则与性能影响。文章指出,从SCOS 7.3版本起允许混插,但整个SAS环路速率将降至最低设备水平,尤其在12Gb控制器上接入6Gb扩展柜会严重拖累性能。指南提供了清晰的兼容性矩阵、实战操作流程及关键避坑清单,帮助管理员安全、高效地完成存储扩容。

weixin_28314575的博客 459

lattice Diamond Programmer程序下载

Lattice Diamond Programmer(支持Lattice FPGA与CPLD程序下载)

qq_44385725的博客 5188

【Netty源码】NioEventLoop源码剖析

NioEventLoopGroup1.NioEventLoopGroup的类层次图2.NioEventLoopGroup 实例化过程分析: EventLoopGroup(其实是MultithreadEventExecutorGroup) 内部维护一个类型为 EventExecutor children 数组, 其大小是 nThreads, 这样就构成了一个线程池 如果我们在实例化 NioEventL

白夜行 1135

量化策略开发步骤系列(5)前向测试和统计意义

我们在第 4 部分,展示了我们策略的样本外测试并讨论了其结果。用于开发模型的数据是 2018 年至 2021 年的数据,测试数据是 2021 年至 2022 年的数据。但有一种方法可以更好地利用我们的数据。

码上助君的专栏 1850

netty4核心源码分析第五篇一核心篇NioEventLoop.run详解

文章目录总原理图selectNow原理图可唤醒select原理图process原理图 总原理图 selectNow原理图 可唤醒select原理图 process原理图

小麒麟的博客 969

【项目分享】RailTracker: 火车票务数据采集与分析

从12306使用爬虫爬取火车站及车次信息、火车票价 通过本项目,我们将带领访问者手把手完成火车票数据采集、数据清洗、数据集成、可视化的一整个流程!

zhushatong的博客 3345

【牛客算法】小美的数组删除

本文分析了小美数组删除问题的解法,提出了一种逆向遍历求MEX的优化算法。该算法通过从数组末尾开始遍历,逐步构建子数组集合,利用MEX的单调性和增量更新特性,在O(n)时间复杂度内计算所有可能删除方案的最小代价。关键设计包括:1)逆向遍历实现MEX状态继承;2)利用HashSet快速判断元素存在;3)动态比较删除前j个元素和删除整个子数组的代价。算法空间复杂度为O(n),适用于大规模数据。代码实现简洁高效,已被验证为同类问题的最优解之一。

字节卷动 1340

【Netty源码分析摘录】(七)NioEventLoop的执行流程 & 空轮询bug

文章目录1.前言2.NioEventLoop.run()3.轮询事件(select())4.JDK 空轮询的 BUG5.处理网络 IO(processSelectedKeys())6.处理任务(runAllTasks())7.总结 1.前言 在上一篇文章中分析了NioEventLoop的创建以及启动过程的源码,在文章结尾处提到,当NioEventLoop线程启动以后,会一直在一个无限 for 循环中一直循环,至死方休,那么在循环中,NioEventLoop到底在循环处理什么呢?这将是本文分析的重点。同时,还

m0_45406092的博客 735

15、SQL Server查询优化与CPU负载管理

本文深入探讨了SQL Server查询优化与CPU负载管理的关键技术,包括强制连接类型的使用、键查找问题的解决、索引设计的最佳实践、高CPU负载的排查与优化,以及并行性的利弊与调整策略。通过优化查询逻辑、合理设计索引、监控与管理CPU资源,可以显著提升SQL Server的性能和稳定性。适用于数据库开发人员和DBA进行性能调优参考。

pz8901234的博客 80

Netty4.0源码分析:NioEventLoop/NioEventLoopGroup的任务处理流程

一、引言 在上篇文章中我们讨论了NioEventLoop/NioEventLoopGroup的构造过程,这篇文章我们来分析NioEventLoop/NioEventLoopGroup的运行过程。 二、源码解析 在Netty中,NioEventLoop(包括所有的SingleThreadEventExecutor子类)有两个职责:一是作为IO线程处理IO事件,二是作为普通的线程处理通过execute...

APlus 1180

整理 钢琴 约翰·汤普森现代钢琴(大汤)

大汤

TS的博客 4005

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Netty源码分析-NioEventLoop(二)

NioEventLoop源码分析。 EventLoop的本质:内部一个线程,一个有序队列存储,线程源源不断的运行队列中的任务。 register方法把java-nio的channel注册到selector上面。 //把JAVA底层Channel注册到selector上 public void register(final SelectableChannel ch, f...

大新博客 522

廖雪峰Python教程[完整版]

廖雪峰的python教程整理的,已经排好版了,而且还是完整的,适合小白

Netty源码分析(三):NioEventLoop启动

NioEventLoop的启动有两大触发器: 先来说服务端启动绑定端口 NioEventLoop的启动流程 首先,bind方法将具体绑定的操作封装成一个task,然后调用NioEventLoop的execute方法,判断调用execute的线程不是Nio 线程,于是调用startThread方法尝试创建线程,创建线程使用的是线程创建器ThreadPerTaskExecutor,创建完线程后Ni...

博客 491

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

Netty源码分析NioEventLoopGroup和NioEventLoop ——(一)

机缘巧合之下看了Netty的源码,但是这玩意对我来说使用的频率确实不多,为了加深自己的记忆,也为了以后可以更快的上手,打算写几篇博客从源码角度来梳理下netty的主线, 本篇也是第一篇。 在Netty中它的“线程池”和“线程”分别用NioEventLoopGroup和NioEventLoop来表示,通常我们用下面几行代码来配置服务端和客户端的线程池。 //client Bootstrap bootstrap = new Bootstrap(); NioEventLoopGroup grou.

start_mao的博客 406

Netty源码分析----服务启动之开始接收请求及其他细节

(*文章基于Netty4.1.22版本) 在上一篇文章Netty源码分析—-服务启动之Channel初始化中,一开始给出了一个NIO的demo,然后从构造方法开始分析Netty对应的封装操作流程,而这篇文章,而这篇文章会开始分析,当初始化完成之后,Netty是如何开始接收请求的。 先看下上一篇文章NIO的demo中,是如何接收请求的(只保留run方法,其他忽略) public class N...

菜鸟一名 1254

三、netty源码分析之EventLoop

一、EventLoop功能概述 上篇我们分析了EventLoopGroup的核心能力,EventLoopGroup具有执行任务、注册Channel、执行器调度等能力。今天我们来看一下EventLoop。我们先来看看EventLoop的类图关系: 我们可以看到,EventLoop接口继承了EventLoopGroup接口。为什么EventLoop要继承EventLoopGroup呢?从上一篇的分析...

feijianke666的博客 818

Netty源码分析---startThread方法详解

本文解析了Netty中startThread()方法的实现原理。该方法通过状态检查和CAS操作确保线程安全启动,使用ThreadPerTaskExecutor创建并启动新线程。关键点包括:1)状态检查避免重复启动;2)CAS操作保证原子性;3)异常处理恢复状态;4)通过ThreadFactory创建命名线程。该方法最终会执行EventLoop的run()方法进入事件循环。

java_rutu的博客 687

netty源码分析7-NioEventLoop-run方法疑难点

本文分享内容如下 select()和空轮询bug解决分析 EventLoop 中对selectKeys的改造 wakeup分析 select()和空轮询bug解决分析 当select空轮询( selector.select(timeoutMillis); 未等待 timeoutMillis) 执行 次数 达到SELECTOR_AUTO_REBUILD_THRESHOLD(默认512)...

chenchangqun11的专栏 351

netty4源码分析——NioEventLoop

有关NioEventLoop的继承结构如下图: NioEventLoop是Netty的Reactor线程,它的职责如下: 1.    作为服务端Acceptor线程,负责处理客户端的请求接入; 2.    作为客户端Connecor线程,负责注册监听连接操作位,用于判断异步连接结果; 3.    作为IO线程,监听网络读操作位,负责从SocketChannel中读取报文; 4.

风居住的城市 835

netty之关闭服务源码分析

服务既然有启动,那么根据爱因斯坦的相对论,就肯定有停止,所以本文就来看下netty停止服务相关源码分析。

wang0907的博客 514

【Netty 专栏】Netty源码分析之NioEventLoop

点击上方“芋道源码”,选择“置顶公众号”技术文章第一时间送达!源码精品专栏中文详细注释的开源项目Java 并发源码合集RocketMQ 源码合集Sharding-JDBC 源码解析合集...

芋艿V 127
上一篇: Netty源码分析---NioEventLoopGroup深度解析
下一篇: Netty源码分析---NioServerSocketChannel-Unsafe初始化详解
不早睡不改名@
博客等级 码龄5年 536粉丝 306原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值