embassy-futures
主线位置:跨任务主线的旁支。一个任务常常要同时等几件事:channel 来消息,或者超时;按钮按下,或者收到命令。
embassy-futures把几个 future 拼成一个,交给执行器当成一个任务 poll。拼的方式决定了谁赢、输家什么时候被 drop、做到一半的事会不会丢。
flowchart TB poll["Select poll"] --> pa["poll a"] pa -->|Ready| first["Either::First"] pa -->|Pending| pb["poll b"] pb -->|Ready| second["Either::Second"] pb -->|Pending| pend["Pending"] first --> drop["Select drop 时 a b 一起 drop"] second --> drop
embassy-futures 是单独发版的 crate(0.1.2,tag embassy-futures-v0.1.2,2025-08-27),src/ 共 1246 行:select.rs 545 行,join.rs 322 行,block_on.rs 45 行,yield_now.rs 49 行,其余是日志宏 fmt.rs。没有依赖(defmt、log 可选),不依赖执行器,no_std,不用 alloc。实验在 exp-embassy/sync-std/src/bin/select_drop.rs(platform-std 执行器,release,M2 Max、macOS 27、rustc 1.99.0)。
select:按参数顺序 poll
select(a, b) 只是把两个 future 装进 Select,poll 依次 poll:
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = unsafe { self.get_unchecked_mut() };
let a = unsafe { Pin::new_unchecked(&mut this.a) };
let b = unsafe { Pin::new_unchecked(&mut this.b) };
if let Poll::Ready(x) = a.poll(cx) {
return Poll::Ready(Either::First(x));
}
if let Poll::Ready(x) = b.poll(cx) {
return Poll::Ready(Either::Second(x));
}
Poll::Pending
}
- 有偏向:
a永远先被 poll。两个都就绪时a赢,b这一次根本不会被 poll。一个总是就绪的a(比如一个满着的 channel 的receive())会让b永远没有机会。 - 两个子 future 拿到的是同一个
cx,即同一个任务的 waker。不管是哪个子 future 登记的 waker 被叫醒,下一次都会从a开始全部重新 poll 一遍。 - 用
unsafe的Pin::new_unchecked做结构化 pin 投影,所以不要求A: Unpin。Select返回后不会再被 poll(.await之后它就被 drop 了),所以已经Ready的子 future 不会被再次 poll。 - 返回值
Either<A, B>只带赢家的输出。select3到select6是同样的代码展开成更多分支(select5/select6是 0.1.2 的 #3430 加的),返回Either3到Either6。
函数文档说「The other future is dropped」。更准确地说:输家和赢家一样,是 Select 结构体的字段,Select 被 drop 时它们才 drop。select(..).await 写法下,.await 拿到 Ready 之后就 drop 掉这个临时的 Select,所以在下一条语句之前输家已经没了。select_drop.rs 用一个会打印 poll 和 drop 的包装 Noisy 看这个顺序:
println!("2. loser is dropped when the Select is dropped");
let r = select(noisy("timer", Timer::after_millis(10)), noisy("never", core::future::pending::<()>())).await;
println!(" after await, first = {}", r.is_first());
2. loser is dropped when the Select is dropped
poll timer -> Pending
poll never -> Pending
poll timer -> Ready
drop timer
drop never
after await, first = true
第二次 poll 时 timer 就绪,never 这次没被 poll;随后两个都在 after await 之前 drop,顺序是字段的声明顺序。
println!("1. both ready on the first poll");
let r = select(noisy("a", async { 1 }), noisy("b", async { 2 })).await;
println!(" -> {:?}", r.is_first());
两个 async 块第一次 poll 就完成。会打印几行 poll、几行 drop,is_first() 是什么?答案在本页末尾。
select_array 与 select_slice
select_array([Fut; N]) 要求所有元素同一类型,按下标顺序 poll,返回 (输出, 下标);空数组永远 Pending。同一类型意味着要么是同一个 async fn 的多次调用,要么是 core::future::ready 这类具名 future(两个 async {} 块即使一模一样也是不同类型,编译器会报 E0308):
println!("3. select_array: lowest index wins");
let (v, i) = select_array([10, 20, 30].map(core::future::ready)).await;
println!(" -> value {v} index {i}");
3. select_array: lowest index wins
-> value 10 index 0
select_slice 接受 Pin<&mut [Fut]>,SelectSlice 只是借用。它返回时谁也不 drop:输家还在调用方的切片里,下一轮可以再 select_slice 一次,接着等剩下的。这是 embassy-futures 里唯一一个「输家不丢」的 select,适合「N 个通道里谁先来处理谁,然后继续等」的循环。
0.1.1 的 select_slice 签名是 fn select_slice<'a, Fut: Future>(slice: &'a mut [Fut]) -> SelectSlice<'a, Fut>,poll 里对每个元素 Pin::new_unchecked,注释说「Since self is pinned, inner cannot move」。可 self 被 pin 住的只是那个 &mut 引用,不是切片里的 future 本身:SelectSlice 完成后,调用方拿回 &mut [Fut],完全可以在安全代码里把一个已经被 poll 过的 future mem::swap 走,而 async fn 的状态机被 poll 后移动是未定义行为。2024 年 9 月的 #3328(首次进入 embassy-futures 0.1.2,CHANGELOG 记作「Fixed soundness issue in select_slice」,提交「fix: select_slice is unsound. fixes #3320」)把参数改成 Pin<&'a mut [Fut]>,由调用方保证切片里的元素不会再移动(例如用 pin! 固定一个数组再转成切片)。这是一次破坏性的签名修改,却只发了一个补丁版本号。
取消:输家做到一半的事
输家被 drop,它里面的状态就没了。丢不丢东西,取决于输家在被 drop 时是否「拿着」什么:
Channel::receive():ReceiveFuture只有一个&Channel,消息在被Ready返回之前一直在队列里。被 drop 不丢消息。Channel::send(v):消息在SendFuture里,被 drop 时消息跟着 drop,channel 没有任何变化。Signal::wait():只在返回Ready的那次 poll 里取走值,文档写明 cancel-safe。- 自己写的
async fn,在两个.await之间把数据存在局部变量里:第一个.await完成、第二个还没完成时被 drop,局部变量里的数据就丢了。 FairSemaphore::acquire():0.8.0 里被叫醒之后才被 drop,会把唤醒一起丢掉(见 async Mutex 与 Semaphore)。
select_drop.rs 的第 4、5 段。read_frame 读两条消息拼成一帧,放进 20 ms 的 with_timeout 里,channel 里只有一条:
// Reads a 2-message "frame". Not cancel-safe: the first message lives only in a local.
async fn read_frame() -> (u32, u32) {
let a = CH.receive().await;
let b = CH.receive().await;
(a, b)
}
CH.try_send(1).unwrap();
match with_timeout(Duration::from_millis(20), read_frame()).await {
Ok(f) => println!(" frame {:?}", f),
Err(_) => println!(" timeout; channel len now {}", CH.len()),
}
CH.try_send(2).unwrap();
CH.try_send(3).unwrap();
println!(" next frame {:?} (message 1 is gone)", read_frame().await);
for round in 0..2 {
match select(Timer::after_millis(5), CH.receive()).await {
Either::First(()) => println!(" round {round}: timer won, len {}", CH.len()),
Either::Second(v) => println!(" round {round}: got {v}"),
}
CH.try_send(7).unwrap();
}
4. cancel a half-read frame
timeout; channel len now 0
next frame (2, 3) (message 1 is gone)
5. a single receive() as the loser loses nothing
round 0: timer won, len 0
round 1: got 7
超时时消息 1 已经从 channel 里出来、存在 read_frame 的局部变量 a 里,future 一 drop 它就没了,下一帧变成 (2, 3)。单独一个 receive() 输给定时器时,channel 里什么也没少;下一轮那条 7 照样收到。UART 读了一半、DMA 传了一半,都是同一个问题:要么让输家是 cancel-safe 的(单次 receive、单次 read),要么把半成品状态放到 future 外面。
with_timeout 不在这个 crate 里,而在 embassy-time:TimeoutFuture 是一个手写的两路 select,先 poll 被包装的 future、再 poll Timer,超时时 drop 前者(见 Timer、Ticker 与超时)。
join:每次都 poll 一遍
join 系列用 MaybeDone 包住每个子 future:Future(fut)、Done(output)、Gone 三态。generate! 宏生成 Join 到 Join5,poll 把每个 MaybeDone 都 poll 一次(已经 Done 的直接算完成,不再 poll 里面的 future),全部完成时一次取出所有输出:
fn poll(
self: Pin<&mut Self>, cx: &mut Context<'_>
) -> Poll<Self::Output> {
let this = unsafe { self.get_unchecked_mut() };
let mut all_done = true;
$(
all_done &= unsafe { Pin::new_unchecked(&mut this.$Fut) }.poll(cx);
)*
if all_done {
Poll::Ready(($(this.$Fut.take_output()), *))
} else {
Poll::Pending
}
}
和 select 一样,所有子 future 共用任务的 waker,没有「哪个子 future 被叫醒了」的记录:任何一个被叫醒,所有没完成的都再 poll 一遍。子 future 少的时候这比维护每个子任务的唤醒标志便宜,join_array 里 N 很大时就是 O(N) 的浪费。完成的子 future 的输出放在 MaybeDone::Done 里,每个子 future 在 Join 里占「future 和输出两者较大的那个」再加一个判别值。join_array 返回 [T; N],用 MaybeUninit 数组收集输出。
block_on、poll_once 与 yield_now
block_on 不需要执行器:
static VTABLE: RawWakerVTable = RawWakerVTable::new(|_| RawWaker::new(ptr::null(), &VTABLE), |_| {}, |_| {}, |_| {});
// ...
pub fn block_on<F: Future>(mut fut: F) -> F::Output {
// safety: we don't move the future after this line.
let mut fut = unsafe { Pin::new_unchecked(&mut fut) };
let raw_waker = RawWaker::new(ptr::null(), &VTABLE);
let waker = unsafe { Waker::from_raw(raw_waker) };
let mut cx = Context::from_waker(&waker);
loop {
if let Poll::Ready(res) = fut.as_mut().poll(&mut cx) {
return res;
}
}
}
waker 的四个函数都是空的,wake 什么也不做,循环不停地 poll,CPU 100%,文档也是这么写的。它适合在 main 里跑一小段 async 初始化代码,或者在测试里。注意 embassy-time 的 Timer 默认用执行器的集成定时器队列,要从 waker 找回任务(见 定时器队列),block_on 的空 waker 不是执行器造的。sync-std/src/bin/blockon_timer2.rs 在 std 上 block_on(Timer::after_millis(5))(release):
thread 'main' (4818648) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-executor-0.10.0/src/raw/waker.rs:35:5:
unwrap of `try_task_from_waker(waker)` failed: Found waker not created by the Embassy executor. Unless the generic timer queue is enabled, `embassy_time::Timer` only works with the Embassy executor.: NoneError
...
thread '<unnamed>' (4818649) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/embassy-time-0.5.1/src/driver_std.rs:54:36:
called `Result::unwrap()` on an `Err` value: PoisonError { .. }
第一次 panic 发生在 std 驱动持锁的时候,锁被毒化,驱动的后台线程跟着 panic。同一段代码如果整个程序里没有用到 embassy-executor(blockon_timer.rs),连链接都过不去:Undefined symbols ... "___embassy_time_queue_item_from_waker",这个符号由执行器提供。poll_once 用同一个空 waker 只 poll 一次,返回 Poll;本卷几个「看一眼是不是 Pending」的实验用的就是它。
yield_now 第一次 poll 时 wake_by_ref 自己再返回 Pending,第二次返回 Ready:任务重新入队,执行器把这一批里的其他任务都 poll 完,下一批才轮到它。拿它轮询一个条件,执行器同样不会睡。
和 futures crate 的差别
以 sync-std 实验工程解析到的 futures-util 0.3.34 为准:
| embassy-futures 0.1.2 | futures-util 0.3.34 | |
|---|---|---|
select(a, b) | 不要求 Unpin;返回 Either<A::Output, B::Output>,输家随 Select drop | future::select 要求 A: Unpin, B: Unpin,返回 Either<(A::Output, B), (B::Output, A)>,把输家交还调用方 |
| 谁先被 poll | 永远按参数顺序 | future::select 同样先 a;select! 宏文档写「one will be pseudo-randomly selected at runtime」 |
| 多个 future | select3 到 select6、select_array、select_slice;join 到 join5、join_array | select_all、join_all 等,join_all 用 Box<[_]> 或 FuturesOrdered,需要 alloc |
block_on | 空 waker 忙等 | futures-executor 的 block_on 在 std 上用线程 park / unpark |
embassy-futures 的 README 写「Optimized for code size」:没有随机数、没有分配、没有输家的所有权转移,代价是偏向和「输家一定被 drop」。想保留输家,就自己在循环外 pin! 住 future,每轮 select(a.as_mut(), b.as_mut())(Pin<&mut F> 也是 future,drop 它不会 drop 底下的 F),或者用 select_slice。
动脑筋答案
1. both ready on the first poll
poll a -> Ready
drop a
drop b
-> true
只有一行 poll:a 就绪后 Select::poll 立刻返回,b 从来没被 poll 过。然后 Select 被 drop,a、b 依次 drop。is_first() 是 true。换成 futures 的 select! 宏,两个都就绪时赢家是随机的。
小结
select/select3..6/select_array按参数顺序 poll,先就绪的赢,有偏向;所有子 future 共用任务的 waker,每次唤醒都从头 poll。- 输家和赢家一起在
Select被 drop 时 drop,.await写法下就是下一条语句之前;select_slice只借用Pin<&mut [Fut]>,谁也不 drop(0.1.2 修好的不健全就在这个签名上)。 - 输家被 drop 时丢不丢东西看它拿着什么:
receive()不丢,send(v)丢v,两次.await之间存在局部变量里的会丢。 join每次把没完成的子 future 全部 poll 一遍,输出存在MaybeDone里。block_on是空 waker 加死循环,yield_now是叫醒自己一次;都会让 CPU 一直跑。- 整个 crate 不分配、不依赖执行器;
futurescrate 的select要Unpin并交还输家,select!随机选,join_all需要分配。