Skip to content

异步基础

> 理解为什么需要异步编程以及它与同步、多线程的区别。

什么是异步编程?

异步编程是一种并发编程模式,允许程序在等待 I/O 操作时继续执行其他任务,而不是阻塞等待。

生活类比

同步模式:餐厅点餐
┌────────────────────────────────────┐
│ 顾客 A 点餐                         │
│   ↓                                 │
│ 等待厨师做完 A 的餐                 │  ← 阻塞等待
│   ↓                                 │
│ A 取餐离开                         │
│   ↓                                 │
│ 顾客 B 才能点餐                     │  ← B 必须等待 A 完成
└────────────────────────────────────┘

异步模式:餐厅点餐
┌────────────────────────────────────┐
│ 顾客 A 点餐                         │
│   ↓                                 │
│ A 拿号等待(做其他事)             │  ← 非阻塞等待
│   ↓                                 │
│ 顾客 B 立即点餐                     │  ← B 不需等待 A
│   ↓                                 │
│ B 拿号等待                          │
│   ↓                                 │
│ 厨师并发处理 A 和 B                 │  ← 并发执行
│   ↓                                 │
│ A 和 B 分别取餐                     │
└────────────────────────────────────┘

为什么需要异步?

I/O 瓶颈

现代应用程序大部分时间花在等待 I/O 操作上:

rust
// 同步 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 在等待网络
}
▶ Run

问题:

  • CPU 大部分时间闲置
  • 一次只能处理一个客户端
  • 性能瓶颈在 I/O 等待,而非计算

异步解决方案

rust
// 异步 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 时,可以处理其他客户端
}
▶ Run

优势:

  • 在等待 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
use 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耗时
}
▶ Run

多线程代码

rust
use 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
}
▶ Run

异步代码

rust
use 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级别
}
▶ Run

异步编程的适用场景

✅ 适用异步

rust
// 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;
    // 定时执行任务
}
▶ Run

❌ 不适用异步

rust
// 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();  // ❌ 会阻塞整个异步运行时
}
▶ Run

异步编程的核心概念

Future

Future 是一个代表异步操作的结果:

rust
// 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,    // 操作未完成,继续等待
}
▶ Run

异步运行时

异步运行时负责:

  1. 调度异步任务
  2. 管理 I/O 事件
  3. 处理定时器
  4. 执行 Future 的 poll 方法
┌─────────────────────────────────────┐
│         异步运行时                    │
├─────────────────────────────────────┤
│                                     │
│  ┌──────────┐  ┌──────────┐        │
│  │ 任务调度器 │  │ I/O 驱动  │        │
│  └──────────┘  └──────────┘        │
│                                     │
│  ┌──────────┐  ┌──────────┐        │
│  │ 定时器    │  │ 任务队列  │        │
│  └──────────┘  └──────────┘        │
│                                     │
│  负责执行所有异步任务                  │
└─────────────────────────────────────┘

async/await

rust
// async 关键字:定义异步函数
async fn my_async_function() -> i32 {
    // 返回 Future<i32>
    42
}

// await 关键字:等待 Future 完成
async fn main() {
    let result = my_async_function().await;  // 阻塞当前异步任务,但不阻塞线程
    println!("Result: {}", result);
}
▶ Run

第一个异步程序

安装 Tokio

toml
# Cargo.toml
[dependencies]
tokio = { version = "1.0", features = ["full"] }

Hello World

rust
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    println!("Hello async!");
    
    // 异步等待
    sleep(Duration::from_secs(1)).await;
    
    println!("After 1 second!");
}
▶ Run

并发任务

rust
use 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!");
}
▶ Run

输出:

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
// ✅ 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;
}
▶ Run

常见误区

1. async ≠ 并行

rust
// ❌ 错误理解: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;
}
▶ Run

2. await ≠ 阻塞线程

rust
// ✅ await 只阻塞当前异步任务,不阻塞线程
async fn task1() {
    sleep(Duration::from_secs(1)).await;  // task1 等待
    // 但其他任务可以继续执行
}

async fn task2() {
    // 在 task1 await 时,task2 可以执行
    println!("I can run while task1 waits!");
}
▶ Run

3. 不要在异步中调用阻塞函数

rust
// ❌ 错误:阻塞整个运行时
async fn bad_example() {
    std::thread::sleep(Duration::from_secs(1));  // 阻塞整个线程
    // 所有异步任务都会被阻塞
}

// ✅ 正确:使用异步版本
async fn good_example() {
    tokio::time::sleep(Duration::from_secs(1)).await;  // 非阻塞
}
▶ Run

小结

异步编程的核心价值:

  • 高效处理 I/O 等待
  • 单线程处理数千并发
  • 低内存开销

关键概念:

  • Future: 异步操作的抽象
  • async/await: 定义和等待异步操作
  • 运行时: 调度和执行异步任务

选择原则:

  • I/O 密集型 → 异步
  • CPU 密集型 → 多线程
  • 混合场景 → 组合使用

下一步: 下一节我们将深入学习 Future trait 和 async/await 的底层机制。

练习

练习 1:编写第一个异步程序

创建一个异步程序,打印时间:

rust
// TODO: 使用 tokio::time 打印当前时间,每秒打印一次,持续 5 次
▶ Run

练习 2:并发任务

创建 3 个异步任务,分别等待 1、2、3 秒,然后并发执行:

rust
// TODO: 使用 tokio::spawn 并发执行
// TODO: 观察完成顺序
▶ Run

练习 3:异步 vs 同步对比

实现同步和异步版本的文件读取,对比性能:

rust
// TODO: 同步版本读取 10 个文件
// TODO: 异步版本读取 10 个文件
// TODO: 测量并对比耗时
▶ Run