
最近不少人问:“Rust 有 async/await 了,为什么还要学 Tokio?直接用 std 不行吗?”
这个问题问得很好。今天这篇是 Rust Tokio 系列第一篇,我们不直接写代码,先把“为什么需要异步运行时”这个根本问题讲透。弄懂这个,后面的 tokio::spawn、channel、网络编程才会水到渠成。
Rust 在 1.39 版本引入了 async/await 语法,看起来和 Go、JavaScript 很像:
async fn fetch_data() -> String {
// 模拟网络请求
"data from server".to_string()
}
#[tokio::main] // 先不管这个宏
async fn main() {
let data = fetch_data().await;
println!("{}", data);
}但这里有个巨大区别:
Rust 的 async fn 只是把函数编译成一个 Future(状态机),它不会自动运行!
.await 也只是告诉编译器:“这里可能要暂停,等数据ready再继续。” 但谁来“驱动”这个暂停和恢复?谁来监听网络I/O完成事件?谁来调度成千上万的任务?
答案是:没有运行时(Runtime),这些都不会发生。
实际上,如果你直接运行 async fn main 且没有运行时宏,Rust 会报这个错;但如果你在 main 内部调用一个异步函数而不使用 .await(或在同步 main 里用 .await),报错会更复杂。建议强调:Rust 标准库(std)不包含异步执行器,这是核心原因。
这就是 Rust 的哲学:零成本抽象 + 显式一切。语言本身不背运行时的锅,把选择权留给开发者。
假设你要写一个高并发 TCP 服务器,比如聊天室、API 服务、爬虫等。
方案一:同步阻塞 + 多线程
每个客户端连接用一个线程处理:
// 伪代码
loop {
let (stream, _) = listener.accept()?;
std::thread::spawn(move || {
handle_client(stream); // 里面有 read/write,可能阻塞
});
}问题来了:
这不是理论,早期很多 C++/Java 服务都吃过这个亏。
异步的核心思想是:
让线程不要傻等,而是“有事干就干,没事就让出 CPU,去干别的任务”。
当一个任务遇到 I/O(网络读写、定时器、文件等)时,它主动 yield(让出),线程立刻去执行其他就绪的任务。I/O 完成后,运行时再把任务“唤醒”继续执行。
这样,一个线程就能同时“并发”处理成千上万的任务,而不用为每个任务分配一个线程。
这正是 Tokio 要解决的问题。
Tokio 提供了两样核心东西:
Future 执行。两者配合,就形成了完整的异步运行时(Runtime)。
Tokio 提供了两种 runtime 风格,你可以根据场景选择:
current_thread(单线程运行时)multi_thread(多线程运行时,默认)使用方式超级简单:
// 默认多线程
#[tokio::main]
async fn main() { ... }
// 指定单线程
#[tokio::main(flavor = "current_thread")]
async fn main() { ... }
// 手动创建更灵活
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads() // 指定 worker 数量
.enable_all()
.build()?;
rt.block_on(async { ... });异步任务的 yield 是协作式(Cooperative)的。如果一个任务里面写了死循环(loop {})而没有 .await,它会直接“饿死”当前的 worker 线程。
在 Cargo.toml 添加依赖(推荐开启 full 特性,学习阶段方便):
[dependencies]
tokio = { version = "1", features = ["full"] }简单示例(异步打印 + 睡眠):
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Hello, Tokio!");
// 启动 3 个并发任务
let task1 = tokio::spawn(async {
sleep(Duration::from_secs()).await;
println!("Task 1 done");
});
let task2 = tokio::spawn(async {
sleep(Duration::from_secs()).await;
println!("Task 2 done");
});
// 等待所有任务完成
tokio::join!(task1, task2); // 或者用 futures::future::join_all
println!("All tasks completed!");
}运行 cargo run,你会看到任务几乎同时启动,1 秒后 Task 1 先完成,2 秒后 Task 2 完成。整个过程只用了很少的线程,却实现了并发。
这就是异步的魅力:代码像同步一样顺序写,执行却是高并发的。
什么时候不需要 Tokio?
async fn 不等于自动多线程。tokio::task::spawn_blocking 处理 CPU/阻塞操作。.await 持有 std::sync::Mutex(经典坑)。这些我们后续文章会详细避坑。
总结
Rust 的 async/await 是语法糖,Tokio 是让这糖真正甜起来的运行时。它解决了高并发下的线程爆炸、内存浪费、上下文切换等问题,让你用少量线程驱动海量任务,同时保持 Rust 的安全与性能。
掌握 Tokio,你就掌握了 Rust 在后端、网络、分布式系统领域的核心竞争力。
我们下期见!
(完)