在上一篇文章中,我们掌握了多线程并发------通过创建操作系统线程来并行执行任务。但线程有它的局限:每个线程需要 1-8MB 的栈空间,创建和切换线程有不可忽视的开销。当需要同时处理数万个连接时(如 Web 服务器、数据库代理),线程模型会迅速耗尽内存。异步编程通过"单线程内并发执行大量任务"解决了这个问题。Rust 的 async/await 语法让异步代码看起来像同步代码,而 Tokio 作为 Rust 异步生态的事实标准,提供了高性能的运行时。本文从同步与异步的区别讲起,系统讲解 Future trait、Tokio 运行时、异步 I/O、超时与取消,并通过一个并发 HTTP 客户端实战帮你掌握 Rust 异步编程的核心技能。
一、同步 vs 异步:为什么需要异步?
1.1 同步阻塞模型的问题
在同步模型中,每个连接需要一个线程。当线程等待 I/O 时(如等待网络响应),它会被操作系统挂起,但线程资源仍然被占用:
rust
use std::net::TcpListener;
use std::thread;
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_connection(stream); // 阻塞处理
});
}
}
问题:
每个连接一个线程,1 万个连接需要 1 万个线程(约 10GB 栈内存)。
线程切换开销大,上下文切换消耗 CPU。
大量时间花在等待 I/O 上,CPU 利用率低。
1.2 异步非阻塞模型
异步模型使用少量线程(通常等于 CPU 核心数),在单个线程内并发执行大量任务。当任务等待 I/O 时,让出 CPU 给其他任务,而不是阻塞线程:
text
同步:任务A等待I/O → 线程阻塞 → 任务B等待线程 → 任务C排队
异步:任务A等待I/O → 让出 → 任务B执行 → 任务C执行 → I/O完成 → 任务A恢复
优势:
1 万个连接只需要几个线程。
内存占用极低(每个任务只需几百字节)。
CPU 利用率高(不会浪费在等待 I/O 上)。
二、async/await:Rust 的异步语法
2.1 async 函数
async fn 定义一个异步函数,它返回一个 Future:
rust
async fn hello() -> String {
String::from("Hello, async!")
}
关键理解:async fn hello() 的返回类型不是 String,而是 impl Future<Output = String>。调用 hello() 不会立即执行函数体,而是返回一个 Future------Future 是惰性的,只有在被 await 或轮询时才会执行。
2.2 await 表达式
await 等待一个 Future 完成并获取其结果:
rust
async fn main_async() {
let result = hello().await; // 等待 hello 完成
println!("{}", result);
}
执行流程:
hello() 返回一个 Future。
.await 轮询该 Future,直到它返回 Ready。
在轮询过程中,如果 Future 未就绪(如等待 I/O),它会返回 Pending,让出 CPU 给其他任务。
I/O 完成后,运行时重新调度该任务,继续执行。
2.3 Future trait
Future 是异步计算的核心抽象:
rust
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T), // 完成,返回结果
Pending, // 未完成,稍后再轮询
}
核心要点:
poll 被调用时,Future 尝试推进计算。
如果完成,返回 Poll::Ready(T)。
如果未完成,返回 Poll::Pending,并注册一个 Waker,当 Future 可以继续时通知运行时。
Pin 的作用:Pin<&mut Self> 保证 Future 在内存中不会被移动,因为 async 函数编译后会生成一个自引用的状态机。
三、Tokio:Rust 异步运行时
Rust 标准库只定义了 Future trait,不提供运行时。运行时由第三方 crate 提供,其中 Tokio 是绝对的主流。
3.1 安装 Tokio
toml
dependencies
tokio = { version = "1.40", features = "full" }
常用 feature:
rt:基本运行时
rt-multi-thread:多线程运行时
macros:#tokio::main 和 #tokio::test 宏
net:异步网络
fs:异步文件系统
time:异步定时器
sync:异步同步原语
full:所有功能
3.2 启动运行时
rust
#[tokio::main]
async fn main() {
println!("Hello from Tokio!");
}
#tokio::main 宏将 async fn main 转换为同步的 fn main,内部创建 Tokio 运行时并执行 Future:
等价的展开形式:
rust
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
println!("Hello from Tokio!");
});
}
3.3 多线程 vs 当前线程运行时
rust
// 多线程运行时(默认,适合大多数场景)
#[tokio::main(flavor = "multi_thread", worker_threads = 4)]
async fn main() { }
// 当前线程运行时(适合轻量级场景)
#[tokio::main(flavor = "current_thread")]
async fn main() { }
选择建议:
多线程:Web 服务器、CPU 密集型任务、需要并行执行。
当前线程:单核嵌入式、测试、需要精确控制任务执行顺序。
3.4 spawn:启动异步任务
tokio::spawn 将一个 Future 提交到运行时,在后台并发执行:
rust
#[tokio::main]
async fn main() {
let handle = tokio::spawn(async {
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
"任务完成"
});
println!("主任务继续执行...");
let result = handle.await.unwrap();
println!("{}", result);
}
spawn 的约束:Future 必须是 Send + 'static,因为它在多线程运行时中可能被调度到不同线程。
四、异步 I/O
4.1 异步文件操作
rust
use tokio::fs;
#[tokio::main]
async fn main() -> std::io::Result<()> {
// 异步写入
fs::write("hello.txt", "Hello, Tokio!").await?;
// 异步读取
let content = fs::read_to_string("hello.txt").await?;
println!("内容: {}", content);
Ok(())
}
⚠️ 注意:异步文件操作在底层仍然使用线程池(因为大多数操作系统没有真正的异步文件 I/O),但 API 是异步的。
4.2 异步网络
rust
use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[tokio::main]
async fn main() -> std::io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
loop {
let (mut socket, addr) = listener.accept().await?;
println!("新连接: {}", addr);
tokio::spawn(async move {
let mut buf = [0; 1024];
loop {
let n = match socket.read(&mut buf).await {
Ok(0) => return, // 连接关闭
Ok(n) => n,
Err(e) => {
eprintln!("读取错误: {}", e);
return;
}
};
if socket.write_all(&buf[..n]).await.is_err() {
return;
}
}
});
}
}
核心优势:每个连接由 tokio::spawn 处理,但只占用极小的内存,单机可支撑数十万并发连接。
五、超时与取消
5.1 超时控制
rust
use tokio::time::{timeout, Duration};
#[tokio::main]
async fn main() {
let result = timeout(Duration::from_secs(1), async {
tokio::time::sleep(Duration::from_secs(5)).await;
"完成"
}).await;
match result {
Ok(value) => println!("结果: {}", value),
Err(_) => println!("超时!"),
}
}
5.2 select:多路等待
tokio::select! 同时等待多个 Future,返回最先完成的:
rust
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
tokio::select! {
_ = sleep(Duration::from_secs(1)) => {
println!("1 秒任务完成");
}
_ = sleep(Duration::from_secs(2)) => {
println!("2 秒任务完成");
}
}
}
select! 的典型用途:
超时控制:等待任务或超时,谁先完成取谁。
优雅关闭:等待正常任务或关闭信号。
竞速:多个数据源,取最快返回的。
5.3 取消安全性
Rust 的异步任务在 await 点可以被取消。当 select! 返回时,未完成的 Future 会被丢弃:
rust
async fn cancel_safe_operation() {
// 在 await 点之间没有副作用,取消是安全的
let data = fetch_data().await;
process(data).await;
}
// ⚠️ 取消不安全:await 之间可能有副作用
async fn cancel_unsafe_operation() {
send_request().await;
// 如果在这里被取消,请求已发送但响应未处理
handle_response().await;
}
六、异步同步原语
Tokio 提供了异步版本的同步原语,用于在异步任务之间共享状态:

6.1 tokio::sync::Mutex vs std::sync::Mutex
rust
use std::sync::Arc;
use tokio::sync::Mutex;
#[tokio::main]
async fn main() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
handles.push(tokio::spawn(async move {
let mut num = counter.lock().await; // 异步获取锁
*num += 1;
}));
}
for handle in handles {
handle.await.unwrap();
}
println!("计数: {}", *counter.lock().await);
}
选择原则:
锁的持有时间短、不涉及 .await → 用 std::sync::Mutex(性能更好)。
锁的持有时间长、期间需要 .await → 用 tokio::sync::Mutex(避免阻塞运行时)。
6.2 信号量:限制并发数
rust
use std::sync::Arc;
use tokio::sync::Semaphore;
#[tokio::main]
async fn main() {
let semaphore = Arc::new(Semaphore::new(3)); // 最多 3 个并发
let mut handles = vec![];
for i in 0..10 {
let permit = semaphore.clone().acquire_owned().await.unwrap();
handles.push(tokio::spawn(async move {
println!("任务 {} 开始", i);
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
println!("任务 {} 完成", i);
drop(permit); // 释放信号量
}));
}
for handle in handles {
handle.await.unwrap();
}
}
七、实战:并发 HTTP 客户端
综合所学知识,实现一个并发 HTTP 客户端:
rust
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::time::{timeout, Duration};
/// 模拟 HTTP 请求
async fn fetch(url: &str) -> Result<String, String> {
// 模拟网络延迟
tokio::time::sleep(Duration::from_millis(100)).await;
if url.contains("error") {
Err(format!("请求 {} 失败", url))
} else {
Ok(format!("来自 {} 的响应", url))
}
}
/// 带超时和重试的请求
async fn fetch_with_retry(url: &str, max_retries: u32) -> Result<String, String> {
for attempt in 1..=max_retries {
match timeout(Duration::from_secs(2), fetch(url)).await {
Ok(Ok(response)) => return Ok(response),
Ok(Err(e)) => {
eprintln!("第 {} 次尝试失败: {}", attempt, e);
if attempt == max_retries {
return Err(e);
}
}
Err(_) => {
eprintln!("第 {} 次尝试超时", attempt);
if attempt == max_retries {
return Err(format!("{} 超时", url));
}
}
}
tokio::time::sleep(Duration::from_millis(500)).await;
}
Err("未知错误".to_string())
}
#[tokio::main]
async fn main() {
let urls = vec![
"https://api.example.com/users",
"https://api.example.com/orders",
"https://api.example.com/products",
"https://api.example.com/error",
"https://api.example.com/payments",
"https://api.example.com/reviews",
];
// 限制并发数为 3
let semaphore = Arc::new(Semaphore::new(3));
let mut handles = vec![];
for url in urls {
let permit = semaphore.clone().acquire_owned().await.unwrap();
let url = url.to_string();
handles.push(tokio::spawn(async move {
let result = fetch_with_retry(&url, 3).await;
drop(permit);
(url, result)
}));
}
// 收集结果
for handle in handles {
match handle.await {
Ok((url, Ok(response))) => println!("✅ {}: {}", url, response),
Ok((url, Err(e))) => println!("❌ {}: {}", url, e),
Err(e) => eprintln!("任务 panic: {}", e),
}
}
}
代码要点:
Semaphore 限制并发数为 3,避免同时发起过多请求。
timeout 为每个请求设置 2 秒超时。
重试机制:失败后等待 500ms 重试,最多 3 次。
tokio::spawn 并发执行所有请求。
acquire_owned() 获取信号量许可,drop(permit) 释放。
八、同步 vs 异步:如何选择?
维度 多线程(std::thread) 异步(Tokio)
适用场景 CPU 密集型 I/O 密集型
并发规模 数百到数千 数十万
内存开销 每线程 1-8MB 每任务数百字节
编程复杂度 中等 较高(async 传染)
调试难度 中等 较高
生态成熟度 极高 成熟
选择建议:
CPU 密集型(如图像处理、加密计算)→ 多线程 + Rayon。
I/O 密集型(如 Web 服务器、数据库代理)→ 异步 + Tokio。
混合场景 → 异步运行时 + spawn_blocking 执行 CPU 密集任务。
九、小结
同步 vs 异步:同步每连接一线程,异步少量线程处理大量并发。
async/await :async fn 返回 Future,.await 等待其完成。
Future trait:poll 返回 Poll::Ready 或 Poll::Pending,Pin 保证自引用安全。
Tokio 运行时:#tokio::main 启动,多线程运行时适合大多数场景。
异步 I/O:tokio::fs 和 tokio::net 提供异步文件与网络操作。
超时与取消:timeout 设置超时,select! 多路等待。
异步同步原语:tokio::sync::Mutex、Semaphore 等,锁持有时间长时使用。
实战模式:信号量限制并发 + 超时 + 重试 + 任务收集。