Rust in Everything
基于 embassy-futures 0.1.2

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,顺序是字段的声明顺序。

❗IMPORTANT
动脑筋:两个都就绪
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 个通道里谁先来处理谁,然后继续等」的循环。

📝NOTE
冷知识:select_slice 曾经不健全

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.2futures-util 0.3.34
select(a, b)不要求 Unpin;返回 Either<A::Output, B::Output>,输家随 Select dropfuture::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」
多个 futureselect3 到 select6、select_array、select_slice;join 到 join5、join_arrayselect_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 不分配、不依赖执行器;futures crate 的 select 要 Unpin 并交还输家,select! 随机选,join_all 需要分配。

💬 关联讨论

发起讨论
加载中...