功能说明
批量并发任务需要两个能力:按完成顺序收集结果,以及在整体超时后停止剩余工作。逐个 await JoinHandle 会按提交顺序阻塞,慢任务拖住整个批次;JoinSet 提供 join_next,谁先完成谁先返回,配合 timeout 就能在限定时间内拿到已完成结果并中止其余任务。
示例的任务按编号模拟不同耗时,第一次给足一秒全部完成,第二次只给两百毫秒,慢任务被中止,只返回已完成的部分。监督函数返回已排序的结果,便于断言。
关键代码
JoinSet::new 创建任务集合,spawn 接受 Future 并返回任务句柄。join_next 在集合非空时返回 Some,任务出错时返回 Err,这里打印错误后继续。timeout 包裹整个收集循环,超时返回 Err,此时调用 abort_all 中止仍未完成的任务。注意超时后异步块被丢弃,任务集合的借用随之结束,才能继续调用 len 与 abort_all。
使用注意
abort_all 只是发出取消信号,任务需要在 await 点才能响应,阻塞型任务无法被及时中止。已收集的结果在超时后保留,这是有意的部分成功语义。任务 panic 会被 JoinSet 捕获为错误,不会传播到监督者。大量任务同时派生需要配合信号量限流。超时后应等待被中止任务的句柄完成清理,避免资源泄漏。
验证方式
第一次输出 finished 为一、四、九,说明三个任务全部完成并按值排序。第二次输入包含耗时六百毫秒的任务三十,两百毫秒后超时,输出 partial 只包含一和四,并打印被中止的任务数量。把截止时间改成两秒,任务三十也会完成,partial 变为完整结果。
代码示例
use std::time::Duration;
use tokio::task::JoinSet;
use tokio::time::{sleep, timeout};
async fn run_task(id: u32) -> u32 {
sleep(Duration::from_millis(20 * id as u64)).await;
id * id
}
async fn supervise(ids: &[u32], deadline: Duration) -> Vec<u32> {
let mut set = JoinSet::new();
for &id in ids {
set.spawn(run_task(id));
}
let mut results = Vec::new();
let outcome = {
let collect = async {
while let Some(joined) = set.join_next().await {
match joined {
Ok(value) => results.push(value),
Err(error) => eprintln!("task failed: {error}"),
}
}
};
timeout(deadline, collect).await
};
if outcome.is_err() {
eprintln!("deadline reached, aborting {} tasks", set.len());
set.abort_all();
}
results.sort_unstable();
results
}
#[tokio::main]
async fn main() {
let finished = supervise(&[1, 2, 3], Duration::from_secs(1)).await;
println!("finished: {finished:?}");
let partial = supervise(&[1, 2, 30], Duration::from_millis(200)).await;
println!("partial: {partial:?}");
}