3. 使用 tokio::main 搭建异步入口

使用 tokio::main 搭建异步入口 学习卡片

从同步 main 到异步入口

Rust 的 fn main() 本身是同步函数,不能直接 .await。要让 main 成为异步入口,需要先初始化一个异步运行时,再在运行时上驱动顶层 Future 直到完成。#[tokio::main] 宏正是把这两步自动展开:它把 async fn main 的函数体包装成一个 Future,并在同步 main 中调用运行时的 block_on 来执行它。

宏展开后的真实结构

下面这段代码是本节的核心写法:用 #[tokio::main] 标注 async fn main,在函数体内用 tokio::spawn 启动多个异步任务,并通过 JoinHandle 等待它们全部完成。

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

async fn fetch(id: u32, ms: u64) -> u32 {
    sleep(Duration::from_millis(ms)).await;
    println!("task {id} done after {ms}ms");
    id * 10
}

#[tokio::main]
async fn main() {
    // 启动多个异步任务,立即返回 JoinHandle
    let h1 = tokio::spawn(fetch(1, 120));
    let h2 = tokio::spawn(fetch(2, 60));
    let h3 = tokio::spawn(fetch(3, 200));

    // 等待全部任务完成,收集结果
    let (r1, r2, r3) = tokio::join!(h1, h2, h3);

    println!("results: {:?} {:?} {:?}",
        r1.unwrap(), r2.unwrap(), r3.unwrap());
}

三个任务几乎同时被提交到运行时,各自按自己的延时推进;tokio::join! 并发等待三个 JoinHandle,因此总耗时接近最慢的那个任务(约 200ms),而不是三者之和(约 380ms)。

宏展开的等价形式:#[tokio::main] 会把 async fn main 改写成同步 fn main,内部先构建运行时,再调用 runtime.block_on(async { ...原函数体... })。理解这一点,就能明白为什么 main 里可以 .await,以及运行时是在何处被创建的。

运行时初始化配置

默认的 #[tokio::main] 使用多线程运行时。若需要单线程运行时(例如在受限环境或需要确定性调度时),可以显式配置:

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let h = tokio::spawn(async {
        println!("running on current_thread runtime");
    });
    h.await.unwrap();
}

多线程运行时(默认)会把任务分配到多个工作线程上并行执行,适合 CPU 与 IO 混合负载;current_thread 运行时只在当前线程上调度,任务之间仍是并发但不会跨线程并行。选择哪种取决于任务是否依赖多核并行,以及是否要求单线程语义。

任务启动与等待的关系

tokio::spawn 要求被派生的 Future 满足 Send + 'static,因为它可能被移动到其他工作线程执行。它立即返回 JoinHandle,任务在后台开始推进;JoinHandle 本身也是 Future,.await 它即可拿到任务返回值。若任务 panic,.await 会得到 Err(JoinError),因此示例中用 unwrap() 显式暴露失败。

#[tokio::main] 展开 async fn main() { ... } 用户编写的异步入口 fn main() { 同步外壳 Builder::new_multi_thread() 构建运行时 runtime.block_on(future) 驱动顶层 Future 至完成 Tokio 运行时 任务调度器 工作线程池 定时器 / IO 驱动 任务队列 task 1 / task 2 / task 3 就绪后由调度器取出执行 sleep 到期后重新入队 任务与结果 spawn(fetch(1,120)) JoinHandle h1 spawn(fetch(2,60)) JoinHandle h2 spawn(fetch(3,200)) JoinHandle h3 tokio::join!(h1, h2, h3) 并发等待,非串行相加 总耗时 ≈ max(120,60,200) 结果:10 / 20 / 30

验证与观察要点

  • 运行后应看到三个任务的完成顺序与延时一致:60ms 的任务先打印,120ms 次之,200ms 最后。
  • 总耗时接近 200ms 而非 380ms,说明任务确实并发推进,而不是顺序等待。
  • 把 #[tokio::main] 换成 #[tokio::main(flavor = "current_thread")],输出顺序与结果不变,但任务在同一线程上交替推进。
  • 若把 tokio::join! 换成依次 h1.await; h2.await; h3.await;,总耗时仍接近 200ms,因为任务在 spawn 时已开始推进;但若把 spawn 也去掉、直接顺序 await 三个 fetch,总耗时会变成约 380ms。
不要在 async fn main 中执行长时间阻塞的同步操作(如 std::thread::sleep 或重 CPU 计算),这会占住工作线程、拖慢其他任务。阻塞操作应放入 tokio::task::spawn_blocking,或改用异步等价 API(如 tokio::time::sleep)。