首页 / Rust 入门教程 / 使用 tokio 运行时

Rust 入门教程

使用 tokio 运行时

本教程共 78 篇 · 第 72 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

RustRust 入门教程tokio异步运行时asyncspawnspawn_blocking

本节目标:用社区最主流的 tokio 运行时把异步代码真正跑起来,掌握 #[tokio::main] 入口、tokio::spawn 并发任务、join!/await 等待,并理解为什么 CPU 密集任务要交给 spawn_blocking

上一章我们写了 Future.await,但光有这些还跑不起来——需要一个运行时(runtime)去轮询(poll)这些 Future,驱动它们前进。Rust 标准库不内置运行时,社区里最成熟、用得最广的是 tokio。这一章就带你把它用起来。

1-1 为什么需要运行时

async fn 只是定义了一个”未来要跑的计划”,.await 是计划里的等待点。但谁来在合适的时机唤醒计划、谁来调度多个任务、谁来提供异步的 sleep 和 IO?这些活都由运行时干。你可以把运行时理解成”异步世界的操作系统内核”:它管理线程池、调度任务、处理 IO 事件。

没有运行时,Future 就是一堆不会动的代码。tokio 就是这样一个异步运行时,它提供了执行器、定时器、异步 TCP/UDP、异步文件等能力。

Note

版本说明:截至本教程核对时(来源 crates.io,核对日期 2026-08-07),tokio 的最新稳定版是 1.53.1,其最低支持 Rust 版本(MSRV)为 1.71。本章示例统一按 Rust 1.97.1 / 2024 Edition 编写,与 tokio 1.x 完全兼容。日常开发用 version = "1" 即可自动取到 1.x 最新版。

1-2 引入 tokio

Cargo.toml 里加上依赖。features = ["full"] 会启用 tokio 的全部功能,初学最省事:

[dependencies]
tokio = { version = "1", features = ["full"] }

等进阶了,你可以只开需要的 feature(比如 rt-multi-threadmacrostimenet)来减小编译体积。但前期用 full 没毛病。

1-3 用 #[tokio::main] 写入口

tokio 提供了一个过程宏 #[tokio::main],能把一个普通的 async fn main 自动包装成”先启动运行时、再在里面跑异步主函数”的形式。你写出来像同步代码,背后它都帮你做好了。

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

#[tokio::main]
async fn main() {
    println!("异步主函数开始");
    sleep(Duration::from_secs(1)).await;
    println!("一秒后醒来");
}

注意两件事:第一,main 前面有 #[tokio::main] 宏,且 main 本身是 async fn;第二,这里的 sleep 来自 tokio::time,是异步的——.await 它时会让出线程,不阻塞整个线程。千万别误用标准库的 std::thread::sleep,那会真把线程堵死。

1-4 用 tokio::spawn 并发任务

运行时里可以同时跑多个任务。用 tokio::spawn 把一个 Future 丢进运行时去并发执行,它返回 JoinHandle,可以 .await 等结果:

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

async fn say(word: &str, secs: u64) {
    sleep(Duration::from_secs(secs)).await;
    println!("{}", word);
}

#[tokio::main]
async fn main() {
    let h1 = tokio::spawn(say("任务一完成", 1));
    let h2 = tokio::spawn(say("任务二完成", 2));

    // 等两个任务都结束
    let _ = h1.await;
    let _ = h2.await;

    println!("全部完成");
}

spawn 出来的任务在后台并发跑,不会互相阻塞。let _ = h.await; 是等待它结束;如果你关心返回值或是否 panic,可以用 let r = h.await; 然后处理 Result

Warning

tokio::spawn 产生的任务必须满足 'static(不能借用外部局部变量的引用),因为它可能在别的线程上跑、活得比当前函数久。要传数据进去,用 move 或把数据用 Arc 包好再 clone 进去。

1-5 并发地等多个 Future

如果手头就是几个 Future,除了逐个 await,更地道的是用 tokio::join!futures::join! 让它们真正并发:

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

async fn job(name: &str, secs: u64) {
    sleep(Duration::from_secs(secs)).await;
    println!("{} 完成", name);
}

#[tokio::main]
async fn main() {
    // 三个任务并发,总耗时约等于最慢的那个(2 秒),而非 1+2+3
    tokio::join!(
        job("A", 1),
        job("B", 2),
        job("C", 3),
    );
    println!("全部完成");
}

join! 会并发驱动所有传入的 Future,等它们全部完成。这比顺序 await 快得多——这正是异步的价值。

1-6 CPU 密集任务用 spawn_blocking

tokio 是个协作式调度器:一个异步任务不在 .await 处让出控制权,运行时就没法切去跑别的任务。如果你的任务是个 CPU 密集的大计算(没有 .await),它会一直霸占线程,别的异步任务全被饿死。

解决方法是用 tokio::task::spawn_blocking 把这种阻塞/密集任务丢到一个独立的 OS 线程上去跑,不占用异步调度:

use tokio::task;

#[tokio::main]
async fn main() {
    let handle = task::spawn_blocking(|| {
        // 这里是 CPU 密集的同步计算,会跑在专属线程上
        let mut sum = 0u64;
        for i in 0..10_000_000 {
            sum = sum.wrapping_add(i);
        }
        sum
    });

    let result = handle.await.unwrap();
    println!("计算结果: {}", result);
}
Tip

经验法则:碰到 IO 等待(网络、文件、定时器),用普通异步任务 + .await;碰到长时间计算或调用同步阻塞函数,用 spawn_blocking。这样 tokio 里其它异步任务才不会被拖垮。

1-7 单线程与多线程运行时

#[tokio::main] 默认启动的是多线程运行时:它内部维护一个工作线程池(默认大小等于 CPU 核心数),任务会被调度到不同线程上跑,这对 IO 密集场景最合适。tokio 也提供单线程运行时,所有任务挤在一个线程上,切换更便宜、没有跨线程同步开销,适合嵌入式或某些测试场景。

选择方式是在宏上标注:

#[tokio::main(flavor = "current_thread")]
async fn main() {
    println!("跑在单线程运行时");
}

默认就是 flavor = "multi_thread"。初学者用默认即可,等遇到性能细节(比如想避免任务在多线程间跳跃带来的缓存失效)再考虑切换。不用过早优化。

1-8 任务取消与 JoinHandle

tokio::spawn 返回的 JoinHandle 不仅用来等结果,它的生命周期还和任务绑定。当 JoinHandle 被丢弃时,任务并不会立刻停,而是继续在后台跑(除非整个运行时关了)。如果你想在任务跑飞时主动取消它,可以调用 handle.abort()

Tip

异步里处理”多路等待”的利器是 tokio::select!:它能同时等”正常完成”和”取消信号”,谁先到就走哪条分支,从而优雅地取消任务。比如一个下载任务,可以一边 select! 等下载完成、一边等用户按 Ctrl+C 的取消信号。这超出了本章范围,但记住 select! 是异步并发控制的瑞士军刀。

1-9 运行时只应启动一次

一个容易踩的坑:运行时要在程序入口启动一次,不要在某个函数里反复 #[tokio::main]Runtime::new()。运行时是重量级对象,反复创建既慢又浪费。正确姿势是:在 main#[tokio::main] 启动,里面所有异步逻辑共享同一个运行时;需要把同步函数里跑一小段异步,可以用 tokio::runtime::Handle::current() 拿到当前运行时的句柄去 block_on,而不是新建运行时。

理解这一点,你的 tokio 程序结构就稳了:一个运行时、多个 spawn 任务、用 .awaitjoin! 编排,CPU 密集的活交给 spawn_blocking

1-10 一个简单的完整例子回顾

把本章要点收个尾:用 #[tokio::main] 启动运行时,用 tokio::spawn 派发任务,join! 并发等待,密集计算交 spawn_blocking。这几个零件拼起来,就能写出结构清晰的高并发程序。记住异步的精髓是”在 .await 处让出”,别在异步任务里写不 await 的长循环,否则会饿死其它任务。

Tip

调试异步程序时,如果某些任务迟迟不推进,优先怀疑两点:是不是某个任务卡在不 await 的 CPU 密集循环里,或者是不是在异步上下文里误用了会阻塞线程的同步调用(如 std::thread::sleep、同步的文件读取)。把同步阻塞换成异步版本,问题常迎刃而解。

1-11 小结

tokio 是驱动 async 跑起来的主流运行时。#[tokio::main] 把异步主函数包进运行时;tokio::spawn 派发并发任务;tokio::join! 并发地等;异步 sleeptokio::time::sleep 别用标准库的。CPU 密集或阻塞逻辑请交给 spawn_blocking,保护异步调度不被饿死。下一章我们看异步代码里怎么优雅地处理错误。