等待队列
主线位置:跨任务主线里「满了就登记 waker、对端操作后叫醒」这两步。
Channel::send发现队列满,把当前任务的 waker 存进senders_waker;接收方取走一条,调senders_waker.wake()。存在哪、存几个、存满了怎么办,就是本页的三个类型。
flowchart TB
reg["register w"] --> same{"已有 waker 叫醒同一任务"}
same -->|是| keep["什么也不做"]
same -->|否| kind{"哪种槽"}
kind --> one["WakerRegistration"]
kind --> many["MultiWakerRegistration"]
one --> evict["叫醒旧的 存新的"]
many --> full{"满了"}
full -->|否| push["追加"]
full -->|是| all["全部叫醒 清空 追加"]
embassy-sync/src/waitqueue/ 一共 230 行,四个文件:atomic_waker.rs、atomic_waker_turbo.rs(开 feature turbowakers 时用它替换前者,见 mod.rs)、waker_registration.rs、multi_waker.rs。三个类型都不分配:Waker 本身是两个字(数据指针加 vtable 指针,thumbv7em 上 8 字节),Option<Waker> 借 vtable 指针非空的空位也是 8 字节。
谁用哪一个
| 类型 | 方法签名 | 用在 |
|---|---|---|
AtomicWaker | register(&self)、wake(&self) | embassy-sync 自己不用;HAL 驱动用,例如 embassy-stm32 0.6.0 的 exti/mod.rs 每条 EXTI 线一个 static EXTI_WAKERS: [AtomicWaker; EXTI_COUNT] |
WakerRegistration | register(&mut self)、wake(&mut self) | Channel、PriorityChannel、zerocopy_channel、Pipe 每方向一个;Mutex、RwLock、GreedySemaphore 各一个 |
MultiWakerRegistration<N> | 同上 | Watch(N 个接收者)、PubSubChannel(SUBS 和 PUBS 各一组) |
&self 和 &mut self 的差别是关键。WakerRegistration 和 MultiWakerRegistration 自己不加锁,必须放进外层的 Mutex<M, RefCell<..>> 里用(见 原始互斥);AtomicWaker 自带同步,可以直接是一个 static,在中断处理函数里 wake()。Signal 和 FairSemaphore 不用这三个,各自把 waker 放在自己的状态里(见 Signal、Watch 与 PubSub 和 async Mutex 与 Semaphore)。
AtomicWaker:名字叫 Atomic,里面是临界区
0.8.0 默认的 AtomicWaker 只是 GenericAtomicWaker<CriticalSectionRawMutex> 的包装,真正的实现是 GenericAtomicWaker:
pub struct GenericAtomicWaker<M: RawMutex> {
waker: Mutex<M, Cell<Option<Waker>>>,
}
impl<M: RawMutex> GenericAtomicWaker<M> {
// ...
/// Register a waker. Overwrites the previous waker, if any.
pub fn register(&self, w: &Waker) {
self.waker.lock(|cell| {
cell.set(match cell.replace(None) {
Some(w2) if (w2.will_wake(w)) => Some(w2),
_ => Some(w.clone()),
})
})
}
/// Wake the registered waker, if any.
pub fn wake(&self) {
self.waker.lock(|cell| {
if let Some(w) = cell.replace(None) {
w.wake_by_ref();
cell.set(Some(w));
}
})
}
}
两点:
register覆盖旧的 waker,不叫醒它。注释写明了用途:「The intended use case is to wake tasks from interrupts」,通常只有一个任务在等一个中断源,被覆盖的旧 waker 一般就是同一个任务的。wake用wake_by_ref,叫醒之后把 waker 放回去。之后再来一次中断、再wake()一次,同一个任务会再被叫醒一次,直到它注册一个别的 waker。下面的WakerRegistration::wake则是take()走,叫一次就空了。
Cell<Option<Waker>> 的 replace / set 都在 critical_section 里做,所以名字里的「Atomic」指的是「中断里也能安全调用」,不是无锁。以前它直接写死 critical_section::with,#3559 把它泛化成 GenericAtomicWaker<M>(CHANGELOG 0.6.1 记作「Add GenericAtomicWaker utility」)。0.8.0 之后的 main 上(尚未发版)AtomicWaker 改成了真正的无锁实现,临界区版本另起名 CriticalSectionWaker。
turbowakers 版本(atomic_waker_turbo.rs)才是原子操作:一个 AtomicPtr<()>,register 是一次 store(Release),wake 是一次 load(Acquire) 加 Waker::from_turbo_ptr(ptr).wake(),同样不清空。
core 打补丁Waker::as_turbo_ptr 和 Waker::from_turbo_ptr 不是标准库的 API。#1263(首次进入 embassy-sync 0.2.0)的提交说明写道:「This is a core patch to make wakers 1 word (the task pointer) instead of 2 (task pointer + vtable)」。好处列了四条,其中一条是「AtomicWaker can actually use atomics to load/store the waker, No critical section needed」;坏处是「You have to patch core...」,而且所有不是 embassy-executor 造的 waker 都会在运行期 panic。用法是用一个脚本给 core 打补丁、开 build-std 和 build-std-features = core/turbowakers,再在 embassy-executor、embassy-sync 上开 turbowakers。所以默认的 AtomicWaker 一直是临界区版本。executor 的 Cargo.toml 在 0.10.0 里仍然说这个 feature「Not surfaced in the docs by default due to being an complicated advanced and undocumented feature」。
WakerRegistration:只记一个,换人时叫醒旧的
WakerRegistration 是一个 Option<Waker>。register 的注释几乎把设计取舍说全了:
pub fn register(&mut self, w: &Waker) {
match self.waker {
// Optimization: If both the old and new Wakers wake the same task, we can simply
// keep the old waker, skipping the clone. (In most executor implementations,
// cloning a waker is somewhat expensive, comparable to cloning an Arc).
Some(ref w2) if (w2.will_wake(w)) => {}
_ => {
// clone the new waker and store it
if let Some(old_waker) = mem::replace(&mut self.waker, Some(w.clone())) {
// We had a waker registered for another task. Wake it, so the other task can
// reregister itself if it's still interested.
//
// If two tasks are waiting on the same thing concurrently, this will cause them
// to wake each other in a loop fighting over this WakerRegistration. This wastes
// CPU but things will still work.
//
// If the user wants to have two tasks waiting on the same thing they should use
// a more appropriate primitive that can store multiple wakers.
old_waker.wake()
}
}
}
}
wake 是 take() 再 wake()。
「换人时叫醒旧的」是正确性要求:旧任务可能还在等,直接覆盖它就永远睡下去了;叫醒它,它会再 poll 一次,条件还不满足就重新 register。代价也写在注释里:两个任务等同一个 WakerRegistration 时会互相叫醒,「This wastes CPU but things will still work」。Channel 的发送方、Mutex 的等锁方都只有一个这样的槽,所以多个任务一起等时,执行器不会睡眠,而是在这几个任务之间来回 poll。Channel 一页量到:两个发送方等一个满的 Channel,100 ms 里各被 poll 一百多万次(std)或二十多万次(QEMU)。
will_wake 比较的是 waker 的数据指针和 vtable。embassy 执行器给同一个任务造的 waker,数据指针都是那个任务的 TaskRef(见 状态与运行队列),所以同一个任务反复 register 只在第一次 clone。注释说 clone 一个 waker「comparable to cloning an Arc」,那是对一般执行器而言;embassy 执行器的 vtable 里 clone 只是用同一个指针再造一个 RawWaker,drop 是空函数(embassy-executor/src/raw/waker.rs),clone 几乎没有成本。对 embassy 来说这个检查更要紧的作用是不让任务叫醒自己:没有它,一个任务每次 poll 都会把槽里自己的旧 waker 换出来再叫醒,变成忙等。
MultiWakerRegistration:满了就全部叫醒
MultiWakerRegistration<N> 是一个 heapless::Vec<Waker, N>。register:
pub fn register(&mut self, w: &Waker) {
// If we already have some waker that wakes the same task as `w`, do nothing.
// This avoids cloning wakers, and avoids unnecessary mass-wakes.
for w2 in &self.wakers {
if w.will_wake(w2) {
return;
}
}
if self.wakers.is_full() {
// All waker slots were full. It's a bit inefficient, but we can wake everything.
// Any future that is still active will simply reregister.
// This won't happen a lot, so it's ok.
self.wake();
}
if self.wakers.push(w.clone()).is_err() {
// This can't happen unless N=0
// (Either `wakers` wasn't full, or it was in which case `wake()` empied it)
panic!("tried to push a waker to a zero-length MultiWakerRegistration")
}
}
先线性扫一遍去重(N 通常是个位数),满了就调 wake 把所有人叫醒、清空,再放进新的。wake 先把长度置 0 再逐个按值取出来叫醒,注释说这是为了 panic=unwind 下不让别人看到一个半清空的 Vec。
2023 年 5 月的 #1490(首次进入 embassy-sync 0.3.0)之前,MultiWakerRegistration<N> 是 [WakerRegistration; N],register 找第一个空槽放进去,找不到就返回 Err(w),由调用方处理:PubSubChannel 里有两个函数 register_subscriber_waker / register_publisher_waker,收到 Err 就全部叫醒再注册一次,注释说满了「can only happen when there was a subscriber that now has dropped」。旧的 register 不做 will_wake 检查,同一个任务每 poll 一次就占一个新槽。提交「sync: do will_wake check in MultiWakerRegistration.」把去重和「满了全叫醒」都挪进了 register,存储换成 heapless::Vec<Waker, N>,register 不再返回 Result,PubSub 里的两个辅助函数随之删掉。
什么时候会满?Watch<M, T, N> 最多发 N 个 Receiver,PubSubChannel 最多 SUBS 个订阅者,正常情况下正好够用。多出来的来自「已经不等了、waker 还留在里面」的情况:一个订阅者在等消息时被 drop(比如作为 select 的输家),它的 waker 不会被移除;之后又新建了一个订阅者来等,槽就满了。满了以后多叫醒的那几个任务只是白 poll 一次,所以注释说「It's a bit inefficient」,不影响正确性。
sync-std/src/bin/multi_waker.rs 用 std::task::Wake 造三个会打印自己名字的 waker:
let (a, b, c) = (waker("A"), waker("B"), waker("C"));
let mut reg = MultiWakerRegistration::<2>::new();
println!("register A"); reg.register(&a);
println!("register A again"); reg.register(&a);
println!("register B"); reg.register(&b);
println!("register C (slots full)"); reg.register(&c);
println!("wake()"); reg.wake();
println!("wake() again"); reg.wake();
let mut one = WakerRegistration::new();
println!("WakerRegistration: register A"); one.register(&a);
println!("WakerRegistration: register B"); one.register(&b);
println!("WakerRegistration: wake()"); one.wake();
每一行 println! 之后会打印哪些「woken: X」?答案在本页末尾。
和 tokio 对照
tokio 的 AtomicWaker 是一个无锁的状态机(REGISTERING / WAKING 位),等待多个任务的地方用侵入式链表,节点放在每个等待者的 future 里,所以等待者数量没有上限,也不会互相叫醒(见 tokio 卷的 batch_semaphore)。embassy 这里只有定长数组和单槽:没有链表、没有 unsafe 的节点生命周期管理,代码很短,代价是等待者多于槽数时退化成「叫醒所有人,让他们自己重新排」。
动脑筋答案
sync-std,release,M2 Max、macOS 27、rustc 1.99.0:
register A
register A again
register B
register C (slots full)
woken: A
woken: B
wake()
woken: C
wake() again
WakerRegistration: register A
WakerRegistration: register B
woken: A
WakerRegistration: wake()
woken: B
register A again:will_wake认出是同一个 waker(槽里存的是a的 clone,指向同一个Arc),直接返回,不占第二个槽。register C:两个槽已满,先wake()叫醒 A 和 B(按存入顺序),清空,再放进 C。- 第一次
wake()只剩 C;第二次wake()时Vec已经空了,什么也不打印。 WakerRegistration只有一个槽:register B时把 A 换出来并叫醒它;wake()叫醒 B 并清空。
小结
AtomicWaker:&self接口,可以是static,给中断驱动用;register覆盖不叫醒,wake叫醒后保留。0.8.0 默认实现是临界区里的Cell<Option<Waker>>,真正原子的版本要turbowakers,而那要给core打补丁。WakerRegistration:一个Option<Waker>,同一任务重复注册不 clone;换成别的任务时叫醒旧的。两个任务等同一个槽会互相叫醒,CPU 不睡。MultiWakerRegistration<N>:heapless::Vec<Waker, N>,先去重,满了全部叫醒再放新的。- 三者都不分配,都靠
will_wake省掉 clone;没有排队顺序,也没有「只叫醒一个」的语义。