Only a single Waker can be active per Poll instance
主线位置:事件主线的旁路。
Waker::new在 debug 下先查Registry里的has_waker标志,再到平台 selector 里建唤醒通道;第二个Waker在第一步就 panic。release 下跳过检查,两个Waker在内核里是一个还是两个,由平台决定。
flowchart TB
second["第二次 Waker::new"] --> dbg{debug_assertions}
dbg -->|是| boom["panic"]
dbg -->|否| plat{平台}
plat -->|epoll| two["两个 token 都到"]
plat -->|kqueue| one["只剩后一个 token"]
plat -->|"poll(2) 后备"| slot["后一个覆盖前一个"]
fix["一个 Arc Waker"] --> ok["原因记在位图里"]
检查怎么实现、各平台的 Waker 是什么、两个 Waker 同时 wake 的结果,在 Waker 里都讲过并跑过。这一页看程序里通常是怎么走到第二个 Waker 的,以及正确的写法。
现象
debug 构建里 Waker::new panic:
thread 'main' (2294090) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.2.4/src/poll.rs:727:9:
Only a single `Waker` can be active per `Poll` instance
release 构建不报任何错,但在 macOS 上有的唤醒会带错 token,在 Linux 上一切正常。只在 Linux 上跑 release 测试的话,这个误用看不出来。
常见的两种写法会走到这里:
- 每个组件各建一个。 事件循环里有一个跨线程的消息通道,还有一个「停止」句柄,两者各自
Waker::new(registry, 自己的 token),想靠 token 区分是谁叫醒的。 - drop 了再建。 某个组件重启时 drop 掉旧的
Waker,再用同一个Poll建一个新的。文档说的是「Only a singleWakercan be active perPoll」,旧的已经 drop 了,看起来是合法的。
复现
src/bin/second_waker.rs 依次做这两种写法,最后是修法:
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::Arc;
use std::sync::atomic::{AtomicU8, Ordering};
use std::time::Duration;
use mio::{Events, Poll, Registry, Token, Waker};
// Two parts of a program, each wants to wake the same event loop.
struct Channel {
waker: Waker,
}
struct Shutdown {
waker: Waker,
}
fn tokens(poll: &mut Poll) -> Vec<usize> {
let mut events = Events::with_capacity(8);
poll.poll(&mut events, Some(Duration::from_millis(100))).unwrap();
events.iter().map(|e| e.token().0).collect()
}
fn try_new(registry: &Registry, token: Token) -> Option<Waker> {
match catch_unwind(AssertUnwindSafe(|| Waker::new(registry, token))) {
Ok(w) => Some(w.unwrap()),
Err(_) => None,
}
}
const CHANNEL: u8 = 1;
const SHUTDOWN: u8 = 2;
fn main() {
println!("os={} debug_assertions={}", std::env::consts::OS, cfg!(debug_assertions));
// 1. each component creates its own Waker
let mut poll = Poll::new().unwrap();
let chan = Channel { waker: Waker::new(poll.registry(), Token(10)).unwrap() };
match try_new(poll.registry(), Token(11)) {
None => println!("1. second Waker::new: panicked"),
Some(w) => {
let stop = Shutdown { waker: w };
chan.waker.wake().unwrap();
stop.waker.wake().unwrap();
println!("1. channel wakes, shutdown wakes -> tokens {:?}", tokens(&mut poll));
}
}
// 2. drop the first Waker, then create a new one
let mut poll = Poll::new().unwrap();
let first = Waker::new(poll.registry(), Token(10)).unwrap();
drop(first);
match try_new(poll.registry(), Token(11)) {
None => println!("2. Waker::new after dropping the first: panicked"),
Some(w) => {
w.wake().unwrap();
println!("2. Waker::new after dropping the first: ok, wake -> tokens {:?}", tokens(&mut poll));
}
}
// 3. the fix: one shared Waker, a bit per reason
let mut poll = Poll::new().unwrap();
let waker = Arc::new(Waker::new(poll.registry(), Token(10)).unwrap());
let reasons = Arc::new(AtomicU8::new(0));
let wake_for = |bit: u8| {
reasons.fetch_or(bit, Ordering::Release);
waker.wake().unwrap();
};
wake_for(CHANNEL);
wake_for(SHUTDOWN);
let got = tokens(&mut poll);
let bits = reasons.swap(0, Ordering::Acquire);
println!("3. shared Waker, two reasons -> tokens {got:?}, channel={} shutdown={}", bits & CHANNEL != 0, bits & SHUTDOWN != 0);
}
M2 Max、macOS、rustc 1.99.0,debug 构建,标准输出和标准错误合在一起:
os=macos debug_assertions=true
thread 'main' (2294090) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.2.4/src/poll.rs:727:9:
Only a single `Waker` can be active per `Poll` instance
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
1. second Waker::new: panicked
thread 'main' (2294090) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/mio-1.2.4/src/poll.rs:727:9:
Only a single `Waker` can be active per `Poll` instance
2. Waker::new after dropping the first: panicked
3. shared Waker, two reasons -> tokens [10], channel=true shutdown=true
Linux 6.12.28(Apple container 里的 Alpine,aarch64 musl 静态链接)、rustc 1.99.0 的 debug 构建:标准输出除了第一行是 os=linux 以外相同,标准错误里是同样两条 panic(线程号是 1,路径是交叉编译时的 macOS 路径)。release 构建,三种配置各跑 3 次,输出不变。macOS:
os=macos debug_assertions=false
1. channel wakes, shutdown wakes -> tokens [11]
2. Waker::new after dropping the first: ok, wake -> tokens [11]
3. shared Waker, two reasons -> tokens [10], channel=true shutdown=true
Linux:
os=linux debug_assertions=false
1. channel wakes, shutdown wakes -> tokens [10, 11]
2. Waker::new after dropping the first: ok, wake -> tokens [11]
3. shared Waker, two reasons -> tokens [10], channel=true shutdown=true
Linux 加 RUSTFLAGS="--cfg mio_unsupported_force_poll_poll"(poll(2) 后备):和 macOS 的三行逐字相同,只有第一行是 os=linux。
- 第 1 种:debug 下第二个
Waker::new直接 panic。release 下 Linux 两个 token 都到;macOS 和poll(2)后备只有[11],通道发来的那次唤醒带的是停止句柄的 token。按 token 分派的代码会把「有新消息」当成「该停了」。 - 第 2 种:drop 掉第一个再建,debug 下照样 panic;release 下三种配置都能用。这一条是 debug 检查比文档严格的地方。
- 第 3 种:一个
Waker,三种配置的结果相同:一个事件,两个原因都在位图里。
为什么
Waker::new 在 debug 下先调 Registry::register_waker:
pub(crate) fn register_waker(&self) {
assert!(
!self.has_waker.swap(true, Ordering::AcqRel),
"Only a single `Waker` can be active per `Poll` instance"
);
}
Waker 没有 Drop 实现,整个 crate 里没有把 has_waker 改回 false 的地方,所以第 2 种也 panic。has_waker 是 Arc<AtomicBool>,try_clone 出来的 Registry 共用它,从克隆的注册器建也拦得住(Waker 的冷知识讲了它怎么从 Selector 挪到 Registry 上)。
release 下各平台的结果来自各自的实现,Waker 的动脑筋答案逐条解释过:epoll 上每个 Waker 是一个独立的 eventfd;kqueue 上每次 wake 都提交 (ident 0, EVFILT_USER) 这同一个 knote,后一次 EV_ADD 把 udata 换成自己的 token;poll(2) 后备的 pending_wake_token 是一个 Option<Token>,后写的覆盖先写的。第 2 种在 release 下能用也是这个原因:Linux 上旧的 eventfd 关了,新的另起一个;macOS 上 knote 本来就只有一个。
文档对这种情况的说法是「What happens if multiple Wakers are registered with the same Poll is unspecified.」第 1 种的三种结果都在这句话的范围内。
问: 第 2 种在 release 下三个平台都能用,文档也只说「active」的 Waker 只能有一个。那 debug 下的 panic 是不是可以无视,比如关掉 debug_assertions 跑测试?
答: 关掉它会同时关掉 IoSource 的重复注册检查(I/O source already registered with a Registry),代价比这一条大。而且「drop 了再建」只在 Poll 线程不在乎中间丢掉的那次唤醒时才安全:旧 Waker 在 Linux 上 drop 的那一刻,还没被取走的唤醒也一起没了(wake() 了,poll 却等到超时)。和 Poll 同生共死的一个 Waker,两个问题都不会有。
怎么修
- 一个
Poll只在创建时建一个Waker,包进Arc,谁要叫醒就给谁一个克隆。wake(&self)只要共享引用,Waker是Send + Sync。文档的例子就是这么写的。 - 唤醒原因自己记。 token 只能告诉你「是
Waker叫醒的」。用一个原子位图(上面的第 3 种)、一个 channel 或一个带锁的队列记下原因,poll返回后读取并清零。先写原因、再wake:几次wake合成一个事件(Waker 的「wake 三次,poll 几次」),原因不能靠事件个数数。 - 组件不要自己
Waker::new。 需要叫醒事件循环的组件,在构造时接收一个Arc<Waker>(或者一个封装好的「发消息并唤醒」句柄),而不是接收&Registry自己建。
tokio
tokio 的 I/O 驱动在 Driver::new 里只建一个 Waker(token 是 TOKEN_WAKEUP),放在驱动的 Handle 里,所有需要叫醒驱动的地方都调 Handle::unpark,也就是这一个 Waker 的 wake。tokio 不暴露它的 Poll,用 tokio 的程序碰不到这个坑;直接用 mio 写事件循环才会。驱动醒来之后不看「是谁叫醒的」,TOKEN_WAKEUP 的事件什么都不做(tokio 卷反应器的「两个保留的 token」)。
怎么确认自己没踩到
- 跑 debug 测试。 这是唯一会报错的地方,而且是 panic,不会被
?吞掉。release 测试抓不到。 - 在 macOS 上测「两个来源同时唤醒」。 Linux 上两个
Waker的程序表现正常;macOS(或 Linux 的poll(2)后备)上会丢 token。 - 搜代码。 全项目里
Waker::new应该只出现在创建Poll的那个函数里,紧挨着Poll::new。
小结
- debug 下第二个
Waker::newpanic,消息是Only a single \Waker` can be active per `Poll` instance,出自Registry::register_waker(poll.rs:727`)。 has_waker从不复位:drop 掉第一个再建也 panic,尽管 release 下这样做在三种配置上都能用。- release 下两个
Waker同时wake:Linux epoll 给两个 token,macOS kqueue 和poll(2)后备只给后一个。 - 修法:一个
Poll一个Arc<Waker>,唤醒原因记在自己的位图或队列里。tokio 的驱动就只有一个Waker。