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
标志位。如果wakenUp
为true
,则会强制唤醒Selector
,并处理唤醒请求。
2.2 原子操作
在 Netty 的事件循环机制中,wakenUp
是一个 AtomicBoolean
标志位,用于表示是否需要唤醒 Selector
。Selector
是 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
的值。如果wakenUp
为true
,则再次调用selector.wakeup()
;这种方法虽然解决了 BAD 情况下的问题,但也带来了效率问题,因为它在 OK 情况下也会唤醒
Selector
,尽管此时并不需要唤醒。