← 中文资源

简体中文版本

JoinSet 任务监督器

批量派生异步任务并收集结果,在截止时间到达时中止仍在运行的任务,返回已完成部分。

阅读英文原文

功能说明

批量并发任务需要两个能力:按完成顺序收集结果,以及在整体超时后停止剩余工作。逐个 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:?}");
}