使用 tokio 运行时
本教程共 78 篇 · 第 72 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:用社区最主流的 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-thread、macros、time、net)来减小编译体积。但前期用 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 任务、用 .await 和 join! 编排,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! 并发地等;异步 sleep 用 tokio::time::sleep 别用标准库的。CPU 密集或阻塞逻辑请交给 spawn_blocking,保护异步调度不被饿死。下一章我们看异步代码里怎么优雅地处理错误。