2团
Published on 2025-04-08 / 6 Visits
0
0

Netty中NioEventLoop#run()中wakeUp竟态问题解析

1. 前言

最近在看NioEventLoop#run()实现时,对如下注释产生了兴趣。

             switch (selectStrategy.calculateStrategy(selectNowSupplier, hasTasks())) {
                    case SelectStrategy.CONTINUE:
                        continue;

                    case SelectStrategy.BUSY_WAIT:
                        // fall-through to SELECT since the busy-wait is not supported with NIO

                    case SelectStrategy.SELECT:
                        select(wakenUp.getAndSet(false));

                        // 'wakenUp.compareAndSet(false, true)' is always evaluated
                        // before calling 'selector.wakeup()' to reduce the wake-up
                        // overhead. (Selector.wakeup() is an expensive operation.)
                        //
                        // However, there is a race condition in this approach.
                        // The race condition is triggered when 'wakenUp' is set to
                        // true too early.
                        //
                        // 'wakenUp' is set to true too early if:
                        // 1) Selector is waken up between 'wakenUp.set(false)' and
                        //    'selector.select(...)'. (BAD)
                        // 2) Selector is waken up between 'selector.select(...)' and
                        //    'if (wakenUp.get()) { ... }'. (OK)
                        //
                        // In the first case, 'wakenUp' is set to true and the
                        // following 'selector.select(...)' will wake up immediately.
                        // Until 'wakenUp' is set to false again in the next round,
                        // 'wakenUp.compareAndSet(false, true)' will fail, and therefore
                        // any attempt to wake up the Selector will fail, too, causing
                        // the following 'selector.select(...)' call to block
                        // unnecessarily.
                        //
                        // To fix this problem, we wake up the selector again if wakenUp
                        // is true immediately after selector.select(...).
                        // It is inefficient in that it wakes up the selector for both
                        // the first case (BAD - wake-up required) and the second case
                        // (OK - no wake-up required).

                        if (wakenUp.get()) {
                            selector.wakeup();
                        }
                        // fall through
                    default:
             }

2. 解析

2.1 select(true)以及select(false)区别

  • select(false)

    • 阻塞调用selector.select() 会阻塞当前线程,直到有通道准备好 I/O 操作或者超时;

    • 不立即返回:不会立即返回,而是等待事件发生;

    • 不处理 wakenUp 标志位:不会检查或处理 wakenUp 标志位。

  • select(true)

    • 非阻塞调用:调用selector.selectNow() 会立即返回,不会等待事件发生;

    • 立即返回:无论是否有通道准备好 I/O 操作,都会立即返回;

    • 处理 wakenUp 标志位:在调用 selector.selectNow() 之前,会检查 wakenUp 标志位。如果 wakenUptrue,则会强制唤醒 Selector,并处理唤醒请求。

2.2 原子操作

在 Netty 的事件循环机制中,wakenUp 是一个 AtomicBoolean 标志位,用于表示是否需要唤醒 SelectorSelector 是 NIO 中用于监听 I/O 事件的组件,它通常在事件循环线程中阻塞等待事件。为了唤醒 Selector,Netty 使用 selector.wakeup() 方法,但这个方法相对昂贵,因此 Netty 通过 wakenUp 标志位来减少不必要的唤醒操作。

  • wakenUp.compareAndSet(false, true)

    • 当需要唤醒 Selector 时,Netty 会调用 wakenUp.compareAndSet(false, true)

      • wakenUp 的当前值是否为 false:则值设置为 true,并返回 true,表示设置成功;

      • wakenUp 的当前值已经为 true:不进行更新,返回 false,表示设置失败(因为已经有一个唤醒请求在队列中了)。

  • wakenUp.getAndSet(false)

    • 获取 wakenUp 的当前值,用于判断是否需要唤醒 Selector;

      • 当前值为true:当前有外部线程试图唤醒selector以处理唤醒请求;

      • 当前值为false:当前无请求需要处理,selector需阻塞等待请求。

查看注释中的代码,wakenUp.getAndSet(false) 作用如下:

select(wakenUp.getAndSet(false));
  • 清理wakeUp标志位:在调用 selector.select()selector.selectNow() 之前,使用 wakenUp.getAndSet(false) 来获取当前的唤醒标志,并将其重置为 false ,这确保了在每次选择操作之前,wakenUp 都是干净的,避免了重复唤醒;

  • 处理唤醒请求:如果 wakenUp.getAndSet(false) 返回 true,表示有外部线程请求唤醒,此时会调用 selector.wakeup() 来唤醒阻塞的 Selector

2.3 注释解析

注释中提及竟态问题:

  • BAD CASE:

    • wakenUp.set(false)selector.select(...) 之间,Selector 被唤醒了;

    • 这种情况下,wakenUp 被设置为 true,而随后的 selector.select(...) 会立即返回,导致 wakenUp 在下一轮之前无法被重置为 false

    • 因此,在下一轮循环前wakenUp.compareAndSet(false, true) 调用会失败(唤醒失败),这又进一步后续selector.select(...) 被阻塞。

  • OK CASE:

    • selector.select(...)if (wakenUp.get()) { ... } 之间,Selector 被唤醒了;

    • 这种情况下,wakenUp 被设置为 true,但随后的检查 if (wakenUp.get()) { ... } 会正确处理,不会导致问题。

解决措施:

  • selector.select(...) 调用之后,立即检查 wakenUp 的值。如果 wakenUptrue,则再次调用 selector.wakeup()

  • 这种方法虽然解决了 BAD 情况下的问题,但也带来了效率问题,因为它在 OK 情况下也会唤醒 Selector,尽管此时并不需要唤醒。


Comment