通用定时器队列满了:不报错,忙转
主线位置:定时器主线第 2 步「进定时器队列」。集成队列和通用队列的区别、
ConstGenericQueue的结构见定时器队列。
flowchart TB
t["任务 X schedule_wake"] --> f{"队列满了"}
f -->|否| ok["插入,睡到期"]
f -->|是| pop["pop 最后一项并 wake"]
pop --> y["任务 Y 提前醒"]
y --> chk["没到期"]
chk --> again["Y 再 schedule_wake"]
again --> f
实验 crate exp-embassy/pitb-gq,bin p19_sensors,三个可执行文件只差 feature:--features gq8、--features gq16、不开(集成队列)(release);M2 Max、macOS 27、rustc 1.99.0,负载 4.7。CPU 时间用 /usr/bin/time -p 量。
现象
项目里有一部分代码要在别的执行器上跑,于是给 embassy-time 开了 generic-queue-8。然后加了几个传感器任务,每个一个 Ticker:
#[embassy_executor::task(pool_size = 8)]
async fn sensor(id: u64) {
let mut ticker = Ticker::every(Duration::from_millis(200 + 10 * id));
loop {
ticker.next().await;
TICKS.fetch_add(1, Ordering::Relaxed);
}
}
// main:spawn n 个 sensor,自己 Timer::after_millis(1000).await,打印 tick 总数
7 个传感器和 8 个传感器:
7 sensors + main: 28 ticks in 1005 ms user 0.00 sys 0.00
8 sensors + main: 31 ticks in 1000 ms user 0.64 sys 0.79
8 sensors + main: 31 ticks in 1000 ms user 0.69 sys 0.76
8 sensors + main: 31 ticks in 1000 ms user 0.71 sys 0.74
多一个任务,tick 数照样对,没有 panic、没有日志,但 1 秒墙钟里用掉了 1.4 秒 CPU(std 驱动有一个闹钟线程,所以能超过 1 秒)。在电池供电的板子上,这就是执行器一直不进 WFE。
出处
generic-queue-N 让 embassy-time-queue-utils 用 ConstGenericQueue,一个 heapless::Vec<Timer, N>,N 由 feature 选(第 103 行起)。schedule_wake 在找不到同一个 waker 的项时插入新项,插不进去就:
loop {
match self.queue.push(timer) {
Ok(()) => break,
Err(e) => timer = e,
}
self.queue.pop().unwrap().waker.wake();
}
把最后一项拿出来立刻唤醒。那个任务醒来一看没到期,再 schedule_wake,又挤掉一个。Driver::schedule_wake 的契约只有「到期前可以提前唤醒」这一条退路,没有错误返回,所以队列满了只能这样(时间驱动)。
main 也在等定时器,它也占一项:7 个传感器加 main 正好 8 项;8 个传感器是 9 项,多出的一项让两个任务互相挤。定时器队列用每个任务的 poll 次数量过同一件事:两个任务 500 ms 里各被 poll 近 200 万次。
main 分支上这段代码没有改过(git log embassy-time-queue-utils-v0.3.2..main -- embassy-time-queue-utils/src/queue_generic.rs 为空)。
修法
不需要别的执行器,就不开
generic-queue-*。默认的集成队列把节点放在每个任务头里,任务数多少都不会满:7 sensors + main: 26 ticks in 1005 ms user 0.00 sys 0.00 8 sensors + main: 29 ticks in 1005 ms user 0.00 sys 0.00embassy-time的Cargo.toml也写着:库不要开generic-queue-*,留给最终程序选。依赖里有库开了它,feature 合并后整个程序都用通用队列。确实要通用队列(RTIC、手写的
block_on、mock 驱动的测试),N至少等于同时在等定时器的任务数(main和每个InterruptExecutor上的任务都算),再留余量:embassy-time = { version = "=0.5.1", features = ["std", "generic-queue-16"] }8 sensors + main: 31 ticks in 1005 ms user 0.00 sys 0.00一个任务同时等几个定时器(
select两个Timer)只占一项:同一个 waker 只留最早的时刻。
怎么确认没中招
cargo tree -e features -i embassy-time-queue-utils --offline | grep generic-queue:看是谁打开了generic-queue-*。- 数一数会同时
.await定时器的任务(包括main),和N比。 - 量 CPU:std 上用
/usr/bin/time看 user + sys 是否接近墙钟时间;板子上在__pender或空闲循环里计数,看空闲时是否还在不停地 poll。
小结
- 通用队列满了不报错,把最后一项提前唤醒腾位置;被挤掉的任务回来再挤,执行器忙转,结果仍正确。
- 实测
generic-queue-8下 8 个传感器加main(9 项):1 秒用掉 1.4 秒 CPU;generic-queue-16或集成队列下为 0。 - 不需要就别开
generic-queue-*;要开就让N不小于同时等定时器的任务数。