Busy - Too many instances of this task are already running
主线位置:定时器主线开头,任务在被 poll 之前先要在静态池里占一格。池怎么分配、格子怎么释放,见任务存储;令牌和
Spawner见 Spawner。
flowchart TB
call["handle(id)"] --> find["find_map claim"]
find --> free{"有空格"}
free -->|有| tok["Ok SpawnToken"]
free -->|没有| busy["Err Busy"]
tok --> sp["spawner.spawn"]
sp --> run["任务运行"]
run --> done["Ready 后释放"]
done --> find
实验 crate exp-embassy/pita-std,bin pool_busy、pool_fixed、spawn_old(platform-std,release);M2 Max、macOS 27、rustc 1.99.0,本机负载约 4。
现象
每来一个事件就 spawn 一个处理任务,这是从 tokio 带来的习惯。事件每 20 ms 一个,每个处理 50 ms:
#[embassy_executor::task]
async fn handle(id: u32) {
Timer::after_millis(50).await;
println!("handled {id} at {} ms", Instant::now().as_millis());
}
#[embassy_executor::main]
async fn main(spawner: Spawner) {
for id in 0..3 {
Timer::after_millis(20).await;
println!("event {id} at {} ms", Instant::now().as_millis());
spawner.spawn(handle(id).unwrap());
}
// ...
}
event 0 at 24 ms
event 1 at 46 ms
thread 'main' (5031856) panicked at src/bin/pool_busy.rs:17:30:
called `Result::unwrap()` on an `Err` value: Busy - Too many instances of this task are already running. Check the `pool_size` attribute of the task.
- 第二个事件到来时第一个任务还在等它的 50 ms,
handle只有一格,handle(1)返回Err。 - panic 的位置是调用任务函数的那一行(
handle(id).unwrap(),第 30 列是unwrap),不是spawn。0.10.0 起错误在创建令牌时就返回,Spawner根本没被碰到。 - 芯片上
unwrap失败就是整个程序停下;在 std 上测试时,事件间隔稍大一点就测不出来。
从 0.9 搬过来的代码通常写成 spawner.spawn(handle(0)).unwrap(),在 0.10.0 上编译(bin spawn_old,debug):
error[E0308]: mismatched types
--> src/bin/spawn_old.rs:11:17
|
4 | #[embassy_executor::task]
| ------------------------- the found opaque type
...
11 | spawner.spawn(handle(0)).unwrap();
| ----- ^^^^^^^^^ expected `SpawnToken<_>`, found `Result<SpawnToken<impl Sized>, _>`
...
error[E0599]: no method named `unwrap` found for unit type `()` in the current scope
--> src/bin/spawn_old.rs:11:28
两个错误一起读就清楚了:unwrap 要从 spawn 的外面挪到任务函数调用的后面。
出处
宏为每个任务函数生成一个 static POOL: TaskPool<Fut, N>,N 就是 pool_size,默认 1(展开见宏)。调用任务函数走到 TaskPool::spawn_impl:
fn spawn_impl<T>(&'static self, future: impl FnOnce() -> F) -> Result<SpawnToken<T>, SpawnError> {
match self.pool.iter().find_map(AvailableTask::claim) {
Some(task) => Ok(task.initialize_impl::<T>(future)),
None => Err(SpawnError::Busy),
}
}
claim 给空着的一格置上 SPAWNED 状态位;只有任务的 future 返回 Ready 后才清掉(任务存储的「释放一格」)。SpawnError 只有 Busy 一种,它的 Debug 转发到 Display,所以 unwrap 打出来是一整句话。池是按任务函数分的:两个不同的任务函数各有各的池,互不影响;同一个函数的所有调用共用一个池。
0.9.1 的 SpawnToken 里是 raw_task: Option<TaskRef>。池满时任务函数照样返回一个令牌,只是 raw_task 是 None(SpawnToken::new_failed()),错误要等到 Spawner::spawn(token) -> Result<(), SpawnError> 里才变成 Err(SpawnError::Busy);另有一个 must_spawn 在失败时 panic。#4606 里的提交「executor: return error when creating the spawntoken, not when spawning.」把 raw_task 改成非 Option,任务函数改为返回 Result<SpawnToken<_>, SpawnError>,spawn 不再返回值,new_failed 和 must_spawn 一起删掉。首次进入 embassy-executor 0.10.0,CHANGELOG 一句「Changed: return error when creating the SpawnToken, not when spawning」。上面 spawn_old 的两个错误就是这次改动的直接结果;raw 层 TaskPool::spawn 的注释还停在旧说法上:「a "poisoned" SpawnToken is returned」。
修法
pool_size 按最大并发数定,再把 Err 当成一个正常的分支处理(bin pool_fixed:三格,前三个事件间隔 20 ms,后两个间隔 5 ms,制造一次真正的超额):
#[embassy_executor::task(pool_size = 3)]
async fn handle(id: u32) {
Timer::after_millis(50).await;
println!("handled {id} at {} ms", Instant::now().as_millis());
}
// in main, for each event:
match handle(id) {
Ok(token) => {
println!("event {id} at {t} ms");
spawner.spawn(token);
}
Err(e) => println!("event {id} at {t} ms dropped: {e}"),
}
event 0 at 24 ms
event 1 at 50 ms
handled 0 at 75 ms
event 2 at 75 ms
event 3 at 81 ms
event 4 at 87 ms dropped: Busy - Too many instances of this task are already running. Check the `pool_size` attribute of the task.
handled 1 at 103 ms
handled 2 at 130 ms
handled 3 at 131 ms
event 3拿到的是handle 0刚在 75 ms 释放的那一格;event 4到来时 1、2、3 三格都在用,被丢掉并记一行,程序继续。- 时间戳比 20 / 50 ms 的网格晚几毫秒,这是 std 驱动在本机负载下的抖动,和池无关。
- 每多一格就多一份 future 的静态内存,
pool_size = N的池大小严格是 N 倍(任务存储的实测)。
事件可以排队的场合,另一种做法是固定几个常驻的工作任务,从一个 Channel 里取事件,池永远不会满;但多个工作任务等同一个 Channel 有自己的代价,见几个任务等同一个 Channel / Signal 时 CPU 不睡。
怎么确认没中招
- 搜任务函数调用后面的
.unwrap()/.expect(:grep -rn 'spawn(.*\.unwrap())' src/(在pool_busy.rs上命中第 17 行,pool_fixed.rs上不命中)。每一处问一句:这个任务会不会在上一个实例结束前被再调一次?会的话要么加pool_size,要么处理Err。 - 测试时把事件间隔压到比处理时间短,让池真的满一次。
- 用
llvm-nm -S --size-sort -C <elf> | grep POOL看每个池占多少.bss,pool_size是用 RAM 换并发。
小结
- 每个任务函数一个静态池,默认一格;没有空格时任务函数返回
Err(SpawnError::Busy),unwrap的 panic 指向调用任务函数的那一行。 - 0.10.0 起错误在创建令牌时返回(#4606);0.9 的
spawner.spawn(task()).unwrap()在 0.10.0 上是E0308加E0599。 - 按最大并发设
pool_size,并把Err当正常分支处理。