同步
执行器只管「谁该被 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,大小在编译期定下来。
- 原始互斥:
RawMutex只有一个闭包式的lock;三种实现分别是critical-section、空操作、线程模式断言;NoopRawMutex不是Sync,放不进static;QEMU 上中断里try_send和线程里try_receive抢同一个Channel,换成NoopRawMutex就 panic 在try_borrow_mut。 - 等待队列:
AtomicWaker、WakerRegistration、MultiWakerRegistration<N>三种 waker 槽;单槽的换人时叫醒旧的,多槽的满了就全叫醒;这两条规则决定了后面所有类型的唤醒行为。 - Channel:跨任务主线。
heapless::Deque加发送方、接收方各一个 waker 槽;满了以后多个发送方互相叫醒,100 ms 里被 poll 上百万次,谁先进去不固定;中断里用try_send;PriorityChannel、zerocopy_channel和它们在 0.8.0 里的两个 bug。 - Signal、Watch 与 PubSub:只留最新值的
Signal、多接收者的Watch、每条消息每个订阅者都要收到的PubSubChannel;publish_immediate挤掉旧消息时慢订阅者收到Lagged。 - async Mutex 与 Semaphore:
Mutex、RwLock、GreedySemaphore都只有一个 waker 槽,不保证顺序;FairSemaphore用票号排队;Pipe、OnceLock、LazyLock;FairSemaphore在 0.8.0 里会丢一次唤醒。 - embassy-futures:
select总是先 poll 第一个、输家随Select一起 drop;join每次把没完成的都 poll 一遍;block_on是忙等;和futurescrate 的差别。
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,代码小得多,代价是没有排队顺序。
💬 关联讨论
发起讨论加载中...