4. tokio::spawn 并发执行独立任务

tokio::spawn 并发执行独立任务 学习卡片

tokio::spawn 的任务模型

tokio::spawn 接收一个 Future(通常写成 async 块),把它交给运行时,返回一个 JoinHandle<T>。任务一旦被派生,就与调用者解耦:调用者继续往下走,任务在运行时的调度器上被并发推进。要拿到任务的返回值,需要 .await 对应的 JoinHandle;如果任务 panic,.await 会得到 Err(JoinError)。

关键约束:tokio::spawn 要求 Future 满足 Send + 'static。这意味着任务不能借用栈上的局部变量,跨任务共享数据要用 Arc、clone 或 move 把所有权搬进任务。

主任务 async main 持有 JoinHandle spawn Tokio 运行时 多线程调度器 Task A sleep 300ms → 1 Task B sleep 150ms → 2 Task C sleep 100ms → 3 并发推进 调度时间线(示例) 0ms 150ms 300ms A: sleep 300ms B: 150ms C: 100ms 完成顺序:C → B → A 总耗时 ≈ 300ms(非 550ms) handle.await → Ok(值) 按 spawn 顺序 await 即可收集

完整可运行示例

下面示例派生三个独立任务,每个任务睡眠不同时长后返回一个整数,主任务按派生顺序 .await 收集结果。注意 move 把 id 和 ms 的所有权搬进任务,满足 'static 约束。

use std::time::Instant;
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let start = Instant::now();

    // 派生三个独立任务,返回 JoinHandle<i32>
    let h1 = tokio::spawn(async move {
        sleep(Duration::from_millis(300)).await;
        println!("[A] 完成 @ {:?}", start.elapsed());
        1
    });

    let h2 = tokio::spawn(async move {
        sleep(Duration::from_millis(150)).await;
        println!("[B] 完成 @ {:?}", start.elapsed());
        2
    });

    let h3 = tokio::spawn(async move {
        sleep(Duration::from_millis(100)).await;
        println!("[C] 完成 @ {:?}", start.elapsed());
        3
    });

    // 按派生顺序收集结果
    let r1 = h1.await.expect("task A panicked");
    let r2 = h2.await.expect("task B panicked");
    let r3 = h3.await.expect("task C panicked");

    println!("结果: {} {} {}", r1, r2, r3);
    println!("总耗时: {:?}", start.elapsed());
}

预期输出(时间戳为示例,实际略有浮动):

[C] 完成 @ 100.xxms
[B] 完成 @ 150.xxms
[A] 完成 @ 300.xxms
结果: 1 2 3
总耗时: 300.xxms
完成顺序是 C→B→A,但收集顺序仍是 1 2 3,因为 .await 按代码书写顺序等待。总耗时约等于最慢任务(300ms),而不是三个任务之和(550ms),这正是并发调度的直接证据。

观察并发调度的三个要点

  • 完成顺序 ≠ 派生顺序:短任务先打印,说明运行时在并发推进多个 Future,而不是串行执行。
  • 总耗时取最大值:若把三个 sleep 直接写在主任务里顺序 .await,总耗时会是 550ms。用 spawn 后降到约 300ms。
  • JoinHandle 是结果通道:不 .await 就丢弃 handle,任务仍会运行,但拿不到返回值,也无法感知 panic。

常见错误与修正

错误写法编译/运行表现修正
tokio::spawn(async { println!("{}", local); })编译报错:借用数据逃逸,不满足 'static加 move,或把 local 克隆进任务
在任务里用 std::thread::sleep阻塞工作线程,其他任务被拖慢改用 tokio::time::sleep(...).await
派生后不保存 handle任务照常跑,但结果和 panic 被静默丢弃保留 JoinHandle 并 .await
在 spawn 的任务里 .await 另一个未 spawn 的 Future该 Future 仍在该任务内串行执行需要真正并发就再 spawn 一次
若某个任务 panic,handle.await 返回 Err(JoinError),expect 会让主任务也 panic。生产代码中应显式处理 JoinError,例如记录日志后继续收集其他任务结果。