异步基础
> 理解为什么需要异步编程以及它与同步、多线程的区别。
什么是异步编程?
异步编程是一种并发编程模式,允许程序在等待 I/O 操作时继续执行其他任务,而不是阻塞等待。
生活类比
同步模式:餐厅点餐
┌────────────────────────────────────┐
│ 顾客 A 点餐 │
│ ↓ │
│ 等待厨师做完 A 的餐 │ ← 阻塞等待
│ ↓ │
│ A 取餐离开 │
│ ↓ │
│ 顾客 B 才能点餐 │ ← B 必须等待 A 完成
└────────────────────────────────────┘
异步模式:餐厅点餐
┌────────────────────────────────────┐
│ 顾客 A 点餐 │
│ ↓ │
│ A 拿号等待(做其他事) │ ← 非阻塞等待
│ ↓ │
│ 顾客 B 立即点餐 │ ← B 不需等待 A
│ ↓ │
│ B 拿号等待 │
│ ↓ │
│ 厨师并发处理 A 和 B │ ← 并发执行
│ ↓ │
│ A 和 B 分别取餐 │
└────────────────────────────────────┘为什么需要异步?
I/O 瓶颈
现代应用程序大部分时间花在等待 I/O 操作上:
rust
▶ Run// 同步 HTTP 服务器
fn handle_client(socket: TcpStream) {
// 1. 等待接收请求(耗时 ~10ms)
let request = read_request(&socket); // 阻塞
// 2. 处理请求(耗时 ~1ms)
let response = process(request);
// 3. 等待发送响应(耗时 ~10ms)
write_response(&socket, response); // 阻塞
// 总耗时 ~21ms,但只有 1ms 是实际计算
// 其他 20ms 在等待网络
}问题:
- CPU 大部分时间闲置
- 一次只能处理一个客户端
- 性能瓶颈在 I/O 等待,而非计算
异步解决方案
rust
▶ Run// 异步 HTTP 服务器
async fn handle_client(socket: TcpStream) {
// 1. 异步接收请求
let request = read_request_async(&socket).await; // 非阻塞
// 2. 处理请求
let response = process(request);
// 3. 异步发送响应
write_response_async(&socket, response).await; // 非阻塞
// 在等待 I/O 时,可以处理其他客户端
}优势:
- 在等待 I/O 时处理其他任务
- 单线程可处理数千并发连接
- CPU 利用率显著提高
同步 vs 异步 vs 多线程
性能对比
场景:处理 1000 个 HTTP 请求,每个耗时 20ms(10ms 网络等待 + 1ms 处理)
同步模式(单线程):
┌────────────────────────────┐
│ 请求 1: 21ms │
│ 请求 2: 21ms │
│ ... │
│ 请求 1000: 21ms │
└────────────────────────────┘
总耗时: 1000 × 21ms = 21 秒
多线程模式(4 线程):
┌────────┬────────┬────────┬────────┐
│线程 1 │线程 2 │线程 3 │线程 4 │
│250请求 │250请求 │250请求 │250请求 │
│5.25秒 │5.25秒 │5.25秒 │5.25秒 │
└────────┴────────┴────────┴────────┘
总耗时: 5.25 秒
优点: 并行处理
缺点: 线程开销(内存 ~2MB/线程),上下文切换成本
异步模式(单线程):
┌────────────────────────────┐
│ 并发处理 1000 个请求 │
│ 在等待时处理其他请求 │
└────────────────────────────┘
总耗时: ~1 秒(主要是处理时间)
优点: 高并发、低内存开销
缺点: 代码复杂度高详细对比
| 模式 | 并发机制 | 内存开销 | 上下文切换 | 适用场景 |
|---|---|---|---|---|
| 同步 | 无并发 | 低 | 无 | 简单任务、脚本 |
| 多线程 | OS 线程 | 高(~2MB/线程) | OS 级别,昂贵 | CPU 密集型 |
| 异步 | 用户态任务 | 低(~KB/任务) | 用户态,轻量 | I/O 密集型 |
代码示例对比
同步代码
rust
▶ Runuse std::fs::File;
use std::io::Read;
fn read_file_sync(path: &str) -> String {
let mut file = File::open(path).unwrap(); // 阻塞等待文件打开
let mut content = String::new();
file.read_to_string(&mut content).unwrap(); // 阻塞等待读取
content
}
fn main() {
let content1 = read_file_sync("file1.txt"); // 阻塞
let content2 = read_file_sync("file2.txt"); // 阻塞(等待 file1 完成)
// 总耗时 = file1耗时 + file2耗时
}多线程代码
rust
▶ Runuse std::thread;
fn read_file_thread(path: &str) -> thread::JoinHandle<String> {
thread::spawn(|| {
read_file_sync(path)
})
}
fn main() {
let handle1 = read_file_thread("file1.txt");
let handle2 = read_file_thread("file2.txt");
let content1 = handle1.join().unwrap();
let content2 = handle2.join().unwrap();
// 总耗时 ≈ max(file1耗时, file2耗时)
// 但内存开销 = 2MB × 2
}异步代码
rust
▶ Runuse tokio::fs::File;
use tokio::io::AsyncReadExt;
async fn read_file_async(path: &str) -> String {
let mut file = File::open(path).await.unwrap(); // 非阻塞等待
let mut content = String::new();
file.read_to_string(&mut content).await.unwrap(); // 非阻塞等待
content
}
#[tokio::main]
async fn main() {
let future1 = read_file_async("file1.txt");
let future2 = read_file_async("file2.txt");
// 并发执行
let (content1, content2) = tokio::join!(future1, future2);
// 总耗时 ≈ max(file1耗时, file2耗时)
// 内存开销 ≈ KB级别
}异步编程的适用场景
✅ 适用异步
rust
▶ Run// 1. 网络服务(HTTP、WebSocket)
async fn handle_http_request() {
// 处理数千并发连接
}
// 2. 文件 I/O(大量文件操作)
async fn process_files() {
// 并发读写文件
}
// 3. 数据库操作
async fn query_database() {
// 等待数据库响应时处理其他查询
}
// 4. 定时任务
async fn scheduled_task() {
tokio::time::sleep(Duration::from_secs(60)).await;
// 定时执行任务
}❌ 不适用异步
rust
▶ Run// 1. CPU 密集型计算
fn fibonacci(n: u64) -> u64 {
// 纯计算,无需异步
// 使用多线程更合适
if n <= 1 { n } else { fibonacci(n - 1) + fibonacci(n - 2) }
}
// 2. 简单脚本任务
fn simple_task() {
// 简单任务,异步增加复杂度
let content = std::fs::read_to_string("file.txt").unwrap();
println!("{}", content);
}
// 3. 阻塞库调用
fn legacy_library_call() {
// 使用阻塞的第三方库
// 不要在异步代码中调用阻塞函数
blocking_lib::do_something(); // ❌ 会阻塞整个异步运行时
}异步编程的核心概念
Future
Future 是一个代表异步操作的结果:
rust
▶ Run// Future trait 定义(简化)
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T), // 操作完成
Pending, // 操作未完成,继续等待
}异步运行时
异步运行时负责:
- 调度异步任务
- 管理 I/O 事件
- 处理定时器
- 执行 Future 的
poll方法
┌─────────────────────────────────────┐
│ 异步运行时 │
├─────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 任务调度器 │ │ I/O 驱动 │ │
│ └──────────┘ └──────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ 定时器 │ │ 任务队列 │ │
│ └──────────┘ └──────────┘ │
│ │
│ 负责执行所有异步任务 │
└─────────────────────────────────────┘async/await
rust
▶ Run// async 关键字:定义异步函数
async fn my_async_function() -> i32 {
// 返回 Future<i32>
42
}
// await 关键字:等待 Future 完成
async fn main() {
let result = my_async_function().await; // 阻塞当前异步任务,但不阻塞线程
println!("Result: {}", result);
}第一个异步程序
安装 Tokio
toml
# Cargo.toml
[dependencies]
tokio = { version = "1.0", features = ["full"] }Hello World
rust
▶ Runuse tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
println!("Hello async!");
// 异步等待
sleep(Duration::from_secs(1)).await;
println!("After 1 second!");
}并发任务
rust
▶ Runuse tokio::time::{sleep, Duration};
async fn task(id: u32, duration: u64) {
println!("Task {} started", id);
sleep(Duration::from_secs(duration)).await;
println!("Task {} completed after {}s", id, duration);
}
#[tokio::main]
async fn main() {
// 并发启动多个任务
let t1 = tokio::spawn(task(1, 2));
let t2 = tokio::spawn(task(2, 1));
// 等待所有任务完成
t1.await.unwrap();
t2.await.unwrap();
println!("All tasks done!");
}输出:
Task 1 started
Task 2 started
Task 2 completed after 1s ← 先完成(耗时短)
Task 1 completed after 2s
All tasks done!异步 vs 多线程的选择指南
决策流程
开始
↓
任务类型是什么?
┌─────────────┐
│ │
I/O 密集型 CPU 密集型
↓ ↓
使用异步 使用多线程
↓ ↓
任务数量很大? 需要并行计算?
↓ ↓
使用 Tokio 使用 rayon具体建议
rust
▶ Run// ✅ I/O 密集型 → 异步
// HTTP 服务器、数据库客户端、文件处理
use tokio::net::TcpListener;
use tokio::fs::File;
// ✅ CPU 密集型 → 多线程
// 图像处理、科学计算、数据分析
use rayon::prelude::*;
// ✅ 混合场景 → 异步 + 多线程
// Web 服务器(异步)+ 数据处理(多线程)
async fn handle_request() {
let result = tokio::task::spawn_blocking(|| {
// CPU 密集型任务
process_data()
}).await;
}常见误区
1. async ≠ 并行
rust
▶ Run// ❌ 错误理解:async 会自动并行执行
async fn main() {
let f1 = async_op1(); // 创建 Future,但未执行
let f2 = async_op2(); // 创建 Future,但未执行
f1.await; // 顺序执行
f2.await; // 等待 f1 完成后才执行 f2
}
// ✅ 正确:使用 spawn 或 join! 实现并发
async fn main() {
let f1 = tokio::spawn(async_op1());
let f2 = tokio::spawn(async_op2());
f1.await;
f2.await;
}2. await ≠ 阻塞线程
rust
▶ Run// ✅ await 只阻塞当前异步任务,不阻塞线程
async fn task1() {
sleep(Duration::from_secs(1)).await; // task1 等待
// 但其他任务可以继续执行
}
async fn task2() {
// 在 task1 await 时,task2 可以执行
println!("I can run while task1 waits!");
}3. 不要在异步中调用阻塞函数
rust
▶ Run// ❌ 错误:阻塞整个运行时
async fn bad_example() {
std::thread::sleep(Duration::from_secs(1)); // 阻塞整个线程
// 所有异步任务都会被阻塞
}
// ✅ 正确:使用异步版本
async fn good_example() {
tokio::time::sleep(Duration::from_secs(1)).await; // 非阻塞
}小结
异步编程的核心价值:
- 高效处理 I/O 等待
- 单线程处理数千并发
- 低内存开销
关键概念:
- Future: 异步操作的抽象
- async/await: 定义和等待异步操作
- 运行时: 调度和执行异步任务
选择原则:
- I/O 密集型 → 异步
- CPU 密集型 → 多线程
- 混合场景 → 组合使用
下一步: 下一节我们将深入学习 Future trait 和 async/await 的底层机制。
练习
练习 1:编写第一个异步程序
创建一个异步程序,打印时间:
rust
▶ Run// TODO: 使用 tokio::time 打印当前时间,每秒打印一次,持续 5 次练习 2:并发任务
创建 3 个异步任务,分别等待 1、2、3 秒,然后并发执行:
rust
▶ Run// TODO: 使用 tokio::spawn 并发执行
// TODO: 观察完成顺序练习 3:异步 vs 同步对比
实现同步和异步版本的文件读取,对比性能:
rust
▶ Run// TODO: 同步版本读取 10 个文件
// TODO: 异步版本读取 10 个文件
// TODO: 测量并对比耗时