Rust in Everything
基于 embassy-executor 0.10.0

同步

执行器只管「谁该被 poll」,任务之间怎么交换数据、怎么等对方,是 embassy-sync 的事。这一部分讲 embassy-sync 0.8.0(tag embassy-sync-v0.8.0)和 embassy-futures 0.1.2(tag embassy-futures-v0.1.2,单独发版)。前者 src/ 下 8911 行(含文件内的单元测试),后者 1246 行。两个 crate 都不依赖 embassy-executor,README 都写着「Futures from this crate can run on any executor」;本部分的 std 实验就是在 platform-std 执行器上跑的。

几乎每个类型都是同一个形状:

pub struct Channel<M, T, const N: usize>
where
    M: RawMutex,
{
    inner: Mutex<M, RefCell<ChannelState<T, N>>>,
}

外层的 M: RawMutex 决定「锁」在这个平台上是什么(关中断、什么也不做、断言在线程模式),中间的 RefCell 把重入变成一次 panic 而不是未定义行为,里面的状态放一个定长缓冲区和一到几个 waker 槽。容量、订阅者数、等待队列长度都是 const 泛型参数,整个对象可以是一个 static,大小在编译期定下来。

  1. 原始互斥:RawMutex 只有一个闭包式的 lock;三种实现分别是 critical-section、空操作、线程模式断言;NoopRawMutex 不是 Sync,放不进 static;QEMU 上中断里 try_send 和线程里 try_receive 抢同一个 Channel,换成 NoopRawMutex 就 panic 在 try_borrow_mut。
  2. 等待队列:AtomicWaker、WakerRegistration、MultiWakerRegistration<N> 三种 waker 槽;单槽的换人时叫醒旧的,多槽的满了就全叫醒;这两条规则决定了后面所有类型的唤醒行为。
  3. Channel:跨任务主线。heapless::Deque 加发送方、接收方各一个 waker 槽;满了以后多个发送方互相叫醒,100 ms 里被 poll 上百万次,谁先进去不固定;中断里用 try_send;PriorityChannel、zerocopy_channel 和它们在 0.8.0 里的两个 bug。
  4. Signal、Watch 与 PubSub:只留最新值的 Signal、多接收者的 Watch、每条消息每个订阅者都要收到的 PubSubChannel;publish_immediate 挤掉旧消息时慢订阅者收到 Lagged。
  5. async Mutex 与 Semaphore:Mutex、RwLock、GreedySemaphore 都只有一个 waker 槽,不保证顺序;FairSemaphore 用票号排队;Pipe、OnceLock、LazyLock;FairSemaphore 在 0.8.0 里会丢一次唤醒。
  6. embassy-futures:select 总是先 poll 第一个、输家随 Select 一起 drop;join 每次把没完成的都 poll 一遍;block_on 是忙等;和 futures crate 的差别。
flowchart TB
  raw["RawMutex"] --> bm["blocking Mutex"]
  bm --> cell["RefCell 状态"]
  cell --> buf["定长缓冲区"]
  cell --> wq["waker 槽"]
  wq --> wr["WakerRegistration"]
  wq --> mw["MultiWakerRegistration"]
  buf --> ch["Channel 等"]
  wr --> ch
  mw --> ps["Watch PubSub"]
  ch --> fut["select join"]

一次 Channel::send / receive 是本卷的跨任务主线,在 Channel 一页走完;那一页里的 waker 是执行器造的,它怎么把任务放回运行队列见 状态与运行队列。和 tokio 对照:tokio 的 mpsc 是无锁链表加 FIFO 信号量,embassy 的 Channel 是一把锁加两个单槽 waker,代码小得多,代价是没有排队顺序。

💬 关联讨论

发起讨论
加载中...