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,尽管此时并不需要唤醒。