楼层: 首页/ 软件技术/ Rust 后端技术栈/ Rust 后端生态与 Tokio 异步运行时
01

Rust 后端生态与 Tokio 异步运行时

Why Rust Backend & Tokio Runtime

先回答一个灵魂问题:Java 已经能写后端了,为什么还要学 Rust?答案很简单——在网关、微服务、基础设施、高并发 API 这些场景,Rust 用十分之一的内存做到同样的吞吐,还不会半夜因为 GC 停顿报警。这一章先把生态地图画清楚,再把 Tokio 这个异步底座讲透。

论Rust 写后端的四个杀手锏

一、性能接近 C,但内存安全。Rust 编译成原生机器码,没有解释器、没有 VM、没有 GC。你的 HTTP 处理函数从加载到跑起来,中间没有任何 runtime 层在中间抽成。

二、无畏并发。编译器在编译期就检查数据竞争——两个线程同时改一块内存,这种 bug 在 Rust 里根本编译不过。Java 是靠你自己加 synchronized 和祈祷,Rust 是靠编译器替你把住关。

三、零成本抽象。你写的 async/await 和泛型抽象,编译后跟手写状态机一样快。没有 JIT 预热,没有虚表派发开销,启动就是峰值性能。

四、单文件部署。cargo build --release 出来一个几十 MB 的二进制,拷到服务器上就能跑,不用装 JDK、不用装 Python、不用 npm install。Docker 镜像可以小到 5MB。

主流 Web 框架对比:到底选哪个

Rust 的 Web 框架不像 Spring Boot 那样一家独大,而是春秋战国。下面这张表是 2026 年的真实格局,别被网上三年前的教程忽悠了。

框架定位人话点评
Axum 0.8当前主流首选Tokio 官方出品,模块化设计,基于 tower 中间件生态。提取器(Extractor)机制优雅,文档齐全。新项目默认选它。
Actix-web 4性能极致派 benchmarks 常年最快。内部是 actor 模型,性能没得说,但 API 风格偏底层,学习曲线略陡。追求极致性能选它。
Rocket易用优先派宏路由写起来最舒服,社区老派框架。但异步生态跟进较慢,新项目用得少了。
Warp函数式组合派用 filter 组合路由,函数式风格很漂亮,但调试错误信息头疼。
Tideasync-std 生态面向 async-std 的设计,但 async-std 已经不如 Tokio 活跃,新项目别选。

选择建议:80% 的场景选 Axum。它和 Tokio、tower、sqlx 是一家人,中间件生态最完整,招人也最好招。除非你有极端性能诉求,否则不用纠结 Actix。

Actix-web:和 Axum 到底差在哪

面试常被问"Axum 和 Actix 怎么选"。下面这张对照说清楚——核心机制几乎一一对应,只是 API 风格不同。本页实战用 Axum,但你要能看懂 Actix 代码。

// Actix-web 4 的最小服务(与 Axum 对照看) use actix_web::{web, App, Responder, HttpResponse, HttpServer}; #[derive(serde::Deserialize)] struct PathInfo { user_id: u32 } // 路径提取器,对标 Axum 的 Path async fn hello(web::Path(info): web::Path<PathInfo>) -> impl Responder { HttpResponse::Ok().json(format!("用户 {}", info.user_id)) } #[actix_web::main] // 对标 #[tokio::main],Actix 自带运行时 async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().route("/users/{user_id}", web::get().to(hello)) }) .bind(("127.0.0.1", 8080))?.run().await }
Axum vs Actix-web 对照
概念AxumActix-web 4
路由Router::new().route("/", get(h))App::new().route("/", web::get().to(h))
路径提取Path<T>web::Path<T>
JSON bodyJson<T>web::Json<T>
状态共享with_state(Arc<S>) + State<S>App::app_data(web::Data::new(s))
中间件tower / tower-http layer自带 wrap(),也能接 tower
运行时必须用 TokioActix-rt 自带,也跑在 Tokio 上
性能很快benchmark 常年第一
生态/招人最大,Tokio 亲儿子略小众,偏底层

一句话:语法几乎一对一,Axum 的中间件生态(tower)和招人优势更大,Actix 赢在裸性能。面试能讲出"两者提取器/路由/状态机制一一对应,差别在生态和运行时"就够了。

Tokio:Rust 的异步版 std

Tokio 是什么?一句话:Rust 异步世界的标准库。你写 async fn 只是定义了一段"将来要执行的逻辑",必须有一个运行时(runtime)来真正调度它。Tokio 就是那个运行时——它负责在少量操作系统线程上,调度成千上万个异步任务,谁准备好了就跑谁。

Cargo.toml:引入 Tokio

[dependencies] tokio = { version = "1", features = ["full"] } # 生产环境别用 "full",按需开 features 编译更快: # tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "time", "sync"] }

最小异步程序:#[tokio::main]

// main.rs use std::time::Duration; use tokio::time::sleep; // #[tokio::main] 把普通 main 变成异步运行时入口 // 对标 Java 的 public static void main + 虚拟线程启动 #[tokio::main] async fn main() { println!("Hello, Tokio!"); // tokio::spawn 创建一个后台异步任务 let handle = tokio::spawn(async { sleep(Duration::from_secs(1)).await; "后台任务完成" }); // .await 等待结果,第二个 ? 是处理 JoinError(任务被取消) let result = handle.await.unwrap(); println!("{result}"); }
$ cargo run Hello, Tokio! 后台任务完成

async/await 到底在等什么

论Future trait 与 .await

一个 async fn 返回的是一个 Future——你可以理解成"一个将来会产出值的占位符"。关键认知:Future 拿到手里不会自动执行,必须 .await 或 spawn 才会跑。这跟 JS 的 Promise 不一样——JS 的 Promise 一创建就开始执行了。

调用 .await 时,如果这个 Future 还没准备好(比如网络请求还没回来),当前任务会让出执行权——注意,是让出协程,不是阻塞操作系统线程。Tokio 调度器立刻去跑别的就绪任务,等网络数据到了,再回来继续执行你 .await 后面的代码。

对标 Java:这就是虚拟线程在做的事,但 Rust 是编译期生成状态机,零开销;Java 虚拟线程是运行时挂载和卸载,有一点点开销。

tokio::join! / try_join! / select!:三种并发姿势

async fn fetch_user(id: u32) -> u32 { sleep(Duration::from_millis(100)).await; id * 10 } #[tokio::main] async fn main() { // join! 同时跑三个,等全部完成(对标 CompletableFuture.allOf) let (a, b, c) = tokio::join!( fetch_user(1), fetch_user(2), fetch_user(3), ); println!("join: {a} {b} {c}"); // select! 竞速:谁先完成用谁(对标 CompletableFuture.anyOf) let slow = fetch_user(9); let fast = async { sleep(Duration::from_millis(10)).await; 42 }; tokio::pin!(slow, fast); tokio::select! { r = &mut slow => println!("slow 赢了: {r}"), r = &mut fast => println!("fast 赢了: {r}"), } }
$ cargo run join: 10 20 30 fast 赢了: 42

异步并发原语:别再 std::sync 全家桶

在 async 代码里,千万别用 std::sync::Mutex——它的 lock 会阻塞线程,把 Tokio 的调度器堵死。要用 Tokio 自己的异步版本。

Tokio 类型作用对标 Java
tokio::sync::Mutex异步互斥锁,等待时让出执行权ReentrantLock
tokio::sync::RwLock读写锁,多读单写ReadWriteLock
tokio::sync::Semaphore信号量,控制最大并发数Semaphore
tokio::sync::mpsc多生产者单消费者通道BlockingQueue
tokio::sync::oneshot一次性通道(一问一答)CompletableFuture
tokio::sync::broadcast广播(一对多)Publisher/Subscriber
tokio::task::JoinHandlespawn 返回的句柄,await 拿结果Future<T>

异步 Mutex + mpsc 通道:典型生产者消费者

use std::sync::Arc; use tokio::sync::{mpsc, Mutex}; #[tokio::main] async fn main() { let counter = Arc::new(Mutex::new(0)); let (tx, mut rx) = mpsc::channel::<String>(100); // 两个生产者 for i in 0..2 { let tx = tx.clone(); let counter = counter.clone(); tokio::spawn(async move { tx.send(format!("from worker {i}")).await.unwrap(); let mut n = counter.lock().await; // 异步锁,不堵线程 *n += 1; }); } drop(tx); // 关闭发送端,recv 循环才会退出 while let Some(msg) = rx.recv().await { println!("收到: {msg}"); } println!("最终计数: {}", counter.lock().await); }
坑:在 async 里用 std::thread::sleep

新手抄 Java 代码,写 std::thread::sleep(Duration::from_secs(1)),结果整个 Tokio 调度器被堵死——所有其他任务全卡住。正确姿势:异步代码里一律用 tokio::time::sleep(...).await,它只挂起当前任务,不堵线程。

坑:Tokio 和 async-std 不能混用

两个运行时各跑各的 reactor,你在 Tokio 里拿到一个 async-std 的 TcpStream,它压根不认识 Tokio 的事件循环。选了 Tokio 就全家桶用 Tokio,reqwest、sqlx、axum 都是 Tokio 生态,别三心二意。

记
本章小结

① Rust 后端四件套:性能接近 C、内存安全、无畏并发、单文件部署,天生适合网关和高并发 API。

② Web 框架默认 Axum,追求极致性能再考虑 Actix-web。

③ Tokio 是异步底座:#[tokio::main] 入口、tokio::spawn 起任务、join!/select! 编排并发,锁和通道一律用 tokio::sync 版本。