小明的学习笔记

Rust 核心概念深度拆解

24 分钟阅读

Rust 核心概念深度拆解

Rust 的设计像一组精密齿轮:数据建模(Struct/Enum/Impl)和抽象表达(泛型/宏)是底层物理齿轮,异步并发(Async/Tokio)是建立其上的高阶引擎。

最核心的结构性联系:async/await 在编译期,底层被转换成带数据的 Enum(状态机)。


在内存层面,structenum 都是用来描述“数据集合”的工具,只是组合逻辑不同:

  • struct 是积类型(Product Type / AND): 一个 User 结构体包含 name(字符串)并且(AND) 包含 age(整数)。它在内存中把这几块空间拼接到一起。

  • enum 是和类型(Sum Type / OR): 一个 Message 枚举可以是 Quit(无数据),或者(OR) 可以是 Move {x: i32, y: i32}(包含坐标数据),或者 可以是 Write(String)(包含字符串)。

  • structenum 负责定义内存中的数据形态(AND 和 OR)。

  • impl 负责给特定的数据形态提供具体方法

  • trait 负责定义跨类型的共享行为契约(你可以 impl Trait for Struct,也可以 impl Trait for Enum)。

一、数据与行为分离:Struct、Enum、Impl

面向对象语言(Java/C++)将数据和行为捆绑在 Class 中,使用继承。Rust 彻底抛弃了继承,采用**「数据与行为解耦」**的组合模式。

1.1 Struct(数据)+ Impl(行为)

struct User {
    name: String,
    active: bool,
}
 
impl User {
    // 关联函数(静态方法),常用作构造器 — 无 &self
    fn new(name: &str) -> Self {
        Self { name: name.to_string(), active: true }
    }
 
    // 实例方法 — 带 &self(只读借用)
    fn is_active(&self) -> bool { self.active }
 
    // 可变方法 — 带 &mut self(可写借用)
    fn deactivate(&mut self) { self.active = false; }
 
    // 消耗型方法 — 带 self(获取所有权,调用后原值失效)
    fn into_name(self) -> String { self.name }
}

1.2 带数据的 Enum — 代数数据类型(ADT)

这是 Rust 建模能力的巅峰。每个变体(Variant)可以携带不同类型、不同大小的数据。适合表达「状态排他性」:一个事务在同一时刻只能处于一种状态。

四种携带数据的方式:

enum Message {
    Quit,                            // 不携带数据
    Move { x: i32, y: i32 },         // 结构体风格(命名字段)
    Write(String),                   // 元组风格(单个值)
    ChangeColor(i32, i32, i32),      // 元组风格(多个值)
}
 
// 使用
let msg1 = Message::Quit;
let msg2 = Message::Move { x: 10, y: 20 };
let msg3 = Message::Write("Hello Rust".to_string());
let msg4 = Message::ChangeColor(255, 0, 0);

模式匹配 — 编译器强制穷尽所有变体:

fn process_message(msg: Message) {
    match msg {
        Message::Quit => println!("程序退出"),
        Message::Move { x, y } => println!("移动到: ({}, {})", x, y),
        Message::Write(text) => println!("写入内容: {}", text),
        Message::ChangeColor(r, g, b) => println!("颜色: RGB({},{},{})", r, g, b),
    }
}

递归枚举 — 表达树形结构:

enum Expr {
    Number(i32),
    Add(Box<Expr>, Box<Expr>),   // Box 因为递归类型大小无法在编译期确定
    Mul(Box<Expr>, Box<Expr>),
}
// 可以表达: (1 + 2) * 3
// Expr::Mul(
//     Box::new(Expr::Add(Box::new(Expr::Number(1)), Box::new(Expr::Number(2)))),
//     Box::new(Expr::Number(3))
// )

内存布局:编译器取占用空间最大的变体为基准分配内存,再加一个极小的「标签(Discriminant)」记录当前是哪个变体。既保证内存连续性,又实现多态。

1.3 Option 和 Result — ADT + 泛型的终极组合

// Option: 有值 或 空(替代 null)
enum Option<T> {
    Some(T),
    None,
}
 
// Result: 成功 或 失败(替代异常)
enum Result<T, E> {
    Ok(T),
    Err(E),
}
特性 Rust Java / Go
空值处理 Option<T> 编译器强制处理 None null / nil 运行时爆炸
错误处理 Result<T, E> + ? 操作符 try-catch 异常 / if err != nil
继承 无,用 Trait 组合 Class 继承
多态 枚举变体 + 模式匹配 虚函数表

1.4 Enum + Trait 组合(多态的核心模式)

为 Enum 实现自定义 Trait

trait Drawable {
    fn draw(&self);
    fn area(&self) -> f64;
}
 
enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
    Triangle { base: f64, height: f64 },
}
 
impl Drawable for Shape {
    fn draw(&self) {
        match self {
            Shape::Circle { radius } => println!("画圆,半径 {}", radius),
            Shape::Rectangle { width, height } => println!("画矩形 {}x{}", width, height),
            Shape::Triangle { base, height } => println!("画三角形"),
        }
    }
 
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
            Shape::Triangle { base, height } => base * height / 2.0,
        }
    }
}

为 Enum 实现标准库 Trait

#[derive(Debug, Clone, PartialEq)]
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}
 
impl std::fmt::Display for IpAddr {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            IpAddr::V4(a, b, c, d) => write!(f, "{}.{}.{}.{}", a, b, c, d),
            IpAddr::V6(s) => write!(f, "{}", s),
        }
    }
}

状态机模式(Enum + Trait 的经典组合)

enum State {
    Idle,
    Processing { data: Vec<u8> },
    Done { result: String },
}
 
trait StateMachine {
    fn transition(self) -> State;
}

1.5 数据建模总览

方式 适用场景 运行时开销
普通 Enum 简单常量 极小(仅标签)
带数据 Enum (ADT) 消息、AST、错误类型、状态机 标签 + 最大变体大小
Enum + Trait 跨类型共享行为 静态分发零开销
dyn Trait 异构集合、插件系统 虚函数表查找

推荐学习顺序

  1. 掌握 match + 带数据 enum
  2. 为 enum 实现标准库 Trait(DebugDisplayClone
  3. 用 enum + 自定义 Trait 实现领域模型
  4. 深入 dyn Trait 和对象安全

二、零成本抽象:泛型(Generics)

泛型让你写一份代码适用多种类型,同时不牺牲运行时性能

2.1 基础用法

struct Point<T> {
    x: T,
    y: T,
}
 
impl<T> Point<T> {
    fn x(&self) -> &T { &self.x }
}
 
// 还可以为特定类型写专属实现
impl Point<f64> {
    fn distance_from_origin(&self) -> f64 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

2.2 单态化(Monomorphization)— 零成本的关键

Rust 不在运行时做类型擦除(与 Java 不同)。

  • 如果你同时用了 Point<i32>Point<f64>,编译器在幕后复制两份代码,把 T 硬编码替换为具体类型
  • 代价:二进制体积变大(代码膨胀)
  • 收益:运行时零开销,执行速度等同于手写的针对特定类型的 C 代码
源代码:    Point<T>  →  编译后:  Point_i32  (一份)
                        编译后:  Point_f64  (另一份,独立)

2.3 Trait 约束(Trait Bound)

// 只接受实现了 Display + Clone 的类型
fn print_and_clone<T: std::fmt::Display + Clone>(item: &T) -> T {
    println!("{}", item);
    item.clone()
}
 
// 等价写法(更可读)
fn print_and_clone<T>(item: &T) -> T
where
    T: std::fmt::Display + Clone,
{
    println!("{}", item);
    item.clone()
}

二点五、错误处理的完整链路:泛型 → 自定义类型 → From → ? → thiserror

这些概念的逻辑链路是:泛型提供抽象容器(Result<T, E>)→ 枚举定义具体错误分类 → From Trait 提供错误类型间转换 → ? 操作符利用 From 实现错误的丝滑传播 → thiserror 宏抹平所有样板代码。

2.5.1 From Trait — 类型的数学映射

pub trait From<T> {
    fn from(T) -> Self;
}

From 定义了如何从一种类型无损转换到另一种类型。例如 From<std::io::Error> for AppError 表示:「我知道怎么把一个 IO 错误变成 AppError」。

2.5.2 ? 操作符的底层展开 — 不是魔法

当你写下:

let content = fs::read_to_string(filename)?;

fs::read_to_string 返回 Result<String, std::io::Error>,但外层函数返回 Result<i32, AppError>。类型不匹配,为什么能编译?因为 ? 展开后等价于:

let content = match fs::read_to_string(filename) {
    Ok(val) => val,                          // 成功:取出值,继续执行
    Err(err) => {
        // 【核心】编译器自动调用 From::from() 转换错误类型
        let converted_err = From::from(err); // std::io::Error → AppError
        return Err(converted_err);           // 提早返回
    }
};

大局观总结

  • thiserror#[from] 帮你生成了 impl From<子错误> for AppError
  • ? 操作符在展开后自动调用这个 From::from
  • 编译器报错 the trait From<SomeError> is not implemented for AppError 正是因为缺少对应的转换实现

2.5.3 完整实战:企业级错误处理

use std::fs;
use thiserror::Error;
 
// 1. 用 Enum 定义全局错误集合
// 2. #[derive(Error)] 宏自动实现 Display + std::error::Error
// 3. #[from] 宏属性自动生成 From trait 实现
#[derive(Debug, Error)]
pub enum AppError {
    #[error("文件系统错误: {0}")]
    IoError(#[from] std::io::Error),          // 自动 impl From<std::io::Error>
 
    #[error("数字解析失败: {0}")]
    ParseError(#[from] std::num::ParseIntError), // 自动 impl From<ParseIntError>
 
    #[error("文件内容不能为空")]
    EmptyFile,                                 // 纯业务错误,不包装外部错误
}
 
type AppResult<T> = Result<T, AppError>;
 
fn read_and_parse(filename: &str) -> AppResult<i32> {
    // read_to_string → Result<String, std::io::Error>
    // ? 触发 From::from → AppError::IoError
    let content = fs::read_to_string(filename)?;
 
    if content.trim().is_empty() {
        return Err(AppError::EmptyFile);       // 直接返回业务错误
    }
 
    // parse → Result<i32, ParseIntError>
    // ? 触发 From::from → AppError::ParseError
    let number: i32 = content.trim().parse()?;
    Ok(number * 2)
}

2.5.4 链路全景

你写的代码:                             编译器/宏做的事:
─────────────────────────────────────────────────────────────
enum AppError { ... }           →     基础:定义错误枚举
#[derive(Error)]                →     thiserror: 自动 impl Display + Error
#[from] std::io::Error          →     thiserror: 自动 impl From<std::io::Error>
#[from] ParseIntError           →     thiserror: 自动 impl From<ParseIntError>
fs::read_to_string()?           →     match + From::from(io_err) + return Err(...)
content.trim().parse()?         →     match + From::from(parse_err) + return Err(...)

一句话? 不是魔法,它是 match + From::from + return Err(...) 的语法糖;thiserror 不是魔法,它是帮你把 FromDisplayError 三个 Trait 的样板代码自动生成的构建工具。

2.5.5 更深一层:? 不是编译器硬编码的

上面说 ? 展开为 match + From::from,但这是简化版。真正的底层机制更加解耦:Rust 不是靠编译器对 ResultOption 的「特殊照顾」来判断的,而是把决定权完全交给了类型系统

终极裁判:ControlFlow 枚举

pub enum ControlFlow<B, C> {
    Continue(C),  // 一切正常,继续执行,携带继续的值
    Break(B),     // 出状况了,立刻中断并返回这个错误/空值
}

无论你是什么类型(Result 也好,Option 也好),只要想用 ?,就必须把自己的状态「翻译」成 ControlFlowContinueBreak。编译器只认 ControlFlow,不认具体类型。

核心引擎:Try Trait 的 branch() 方法

pub trait Try {
    type Output;    // 成功时提取出的值的类型(对应 Continue)
    type Residual;  // 失败时提前返回的「残余物」类型(对应 Break)
 
    // 【核心逻辑】:由类型自己决定怎么分支!
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
}

Result 怎么自己做决定?

impl<T, E> Try for Result<T, E> {
    type Output = T;
    type Residual = Result<Infallible, E>;
 
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        match self {
            Ok(v)  => ControlFlow::Continue(v),      // 成功 → 继续
            Err(e) => ControlFlow::Break(Err(e)),     // 失败 → 中断
        }
    }
}

Option 怎么自己做决定?

impl<T> Try for Option<T> {
    type Output = T;
    type Residual = Option<Infallible>;
 
    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
        match self {
            Some(v) => ControlFlow::Continue(v),      // 有值 → 继续
            None    => ControlFlow::Break(None),      // 空 → 中断
        }
    }
}

?真正展开式(编译器级别)

// 你写的:
let val = my_variable?;
 
// 编译器真正生成的(不是对 Result 硬编码,而是统一调用 Try::branch):
let val = match std::ops::Try::branch(my_variable) {
    ControlFlow::Continue(v) => v,                    // 继续,取出值
    ControlFlow::Break(residual) => {
        // FromResidual 负责做类型转换(包括我们熟悉的 From::from)
        return std::ops::FromResidual::from_residual(residual);
    }
};

大局观:三层抽象

你写的代码:         编译器做的:             类型自己做的:
──────────────────────────────────────────────────────────
my_val?         →   调用 Try::branch()  →   Result: Ok→Continue, Err→Break
                    ↓                      Option: Some→Continue, None→Break
                  match ControlFlow:
                  Continue → 取值
                  Break → return

绝妙之处:未来如果 Rust 引入新类型(比如某种新的错误处理类型),只要它实现了 Try Trait 并映射好 ControlFlow,无需修改编译器就能立刻支持 ? 操作符。这是真正的极度解耦

2.5.6 为什么纯 String 不加 Option/Result 外壳就不能用 ?

因为 String 没有实现 Try Trait。纯 String 本身就是一个确定存在的值,没有「成功/失败」或「存在/不存在」的外壳,? 无壳可剥。

fn find_user(id: u32) -> String { "Alice".to_string() }
fn get_user_domain(user_id: u32) -> Option<String> {
    // ❌ let user = find_user(user_id)?;  // 编译报错: Try is not implemented for String
    // ✅ 直接接收,不需要 ?
    let user: String = find_user(user_id);
    let domain = extract_domain(user)?;  // extract_domain 返回 Option,可以用 ?
    Some(domain)
}

编译器会直接报错:the trait Try is not implemented for String——? 是一把专门「开箱」的钥匙,只有面对 ResultOption 这种「薛定谔的盲盒」时才需要且能够使用。


三、代码生成器:宏(Macros)

泛型解决「类型不同但逻辑相同」。宏解决「参数数量不同」或「根据结构体自动生成样板代码」。

宏的本质是元编程(Metaprogramming)——写一段「用来写代码的代码」。

3.1 声明宏(Declarative Macros)— name!

最常见的是 println!vec!。用模式匹配做代码替换,在编译期展开。

// vec! 接受任意数量参数(普通函数做不到)
let v = vec![1, 2, 3];
 
// 编译期展开后等价于:
let v = {
    let mut temp_vec = Vec::with_capacity(3); // 预分配,避免动态扩容
    temp_vec.push(1);
    temp_vec.push(2);
    temp_vec.push(3);
    temp_vec
};

3.2 过程宏(Procedural Macros)

更高级,接收代码 → 修改代码 → 输出新代码。通常体现为属性注解。

// Derive 宏 — 自动生成 impl 逻辑
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
struct User {
    name: String,
    age: u32,
}
// 编译器自动为你实现:Debug 打印、Clone 复制、相等比较、JSON 序列化/反序列化
 
// 属性宏 — 改变函数的本质
#[tokio::main]   // ← 这就是过程宏,改造了 main 函数
async fn main() { /* ... */ }

3.3 宏 vs C 宏:本质区别

维度 C 宏 (#define) Rust 宏
工作层 预处理器,文本替换 编译器,AST 操作
类型安全 ❌ 无,字符串盲替换 ✅ 操作的是类型检查过的 AST
能生成非法语法 ✅ 很容易 ❌ 展开阶段就会报错
操作对象 文本字符 抽象语法树节点

3.4 AST 是什么?

编译器做的第一件事是词法分析 + 语法分析,将平铺文本转化为严格层次结构的抽象语法树

let a = 1 + 2;

AST:
  变量声明语句 (Let)
  ├── 左: 标识符 "a"
  └── 右: 二元表达式 (+)
           ├── 左: 整数字面量 1
           └── 右: 整数字面量 2

3.5 整个 Crate 是一棵巨树

整个 Crate 是一棵极其庞大的、连根拔起的唯一 AST 巨树。 Rust 解析器(Parser)从 src/main.rssrc/lib.rs 第一行开始,把 mod 引入的所有文件全部拼接起来,最终形成一棵唯一的树

Crate (根)
├── Item::Struct (结构体定义)
├── Item::Enum (枚举定义)
├── Item::Trait (trait 定义)
├── Item::Fn (函数)                    ← 一个函数只是树上的一个分枝
│   ├── 标识符: "add"
│   ├── 可见性: Inherited (默认私有)
│   ├── 签名 Signature:
│   │   ├── 参数列表: [参数: a, 类型: i32]
│   │   └── 返回类型: i32
│   └── 函数体 Block:
│       └── 语句列表:
│           └── Expr::Binary (+)
│               ├── 左: Expr::Path("a")
│               └── 右: Expr::Lit(1)
└── ... (更多项)

一句话(Statement)只是这棵大树的一个分叉,绝对不是孤立的树。

3.6 宏操作 AST 的终极意义

由于整个项目是一棵严密的层级树,#[tokio::main] 这样的宏接收到的不是文本,而是一个 Item::Fn 数据结构及其所有子节点。宏可以在内存里:

  1. 读取子节点的名字(如 "add"),改成 "add_async_version"
  2. 在函数体的最外层嫁接一层 tokio::runtime::Builder::new()... 的树枝
  3. 添加或删除参数、插入新的语句

这解释了为什么 Rust 宏是类型安全的:它操作的是有严格层级和类型的「树」,而不是像 C #define 那样对文本字符串做盲目查找替换。如果宏拼接出的树枝不符合 Rust 语法结构规范,在 AST 展开阶段就会立刻报错拦截,绝不会留下一个在运行期才爆炸的定时炸弹。


四、异步基石:Future Trait 与 async/await

4.1 Future Trait — 完全不是魔法

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,    // 还没办完,请稍后再来问我
}

任何结构体只要实现了 Future trait(提供了 poll 方法),就是合法的异步任务。它是对「一段需要跨越时间才能得到结果的逻辑」的标准化抽象。

4.1.1 为什么是 type Output; 而不是泛型 Future<T>

type Output; 在 Rust 中叫关联类型(Associated Type)

  • 如果用泛型 trait Future<T>:意味着可以为一个结构体实现多次 Trait。比如 impl Future<i32> for MyTaskimpl Future<String> for MyTask。这在逻辑上荒谬——一个确定的异步任务,怎么可能既产出整数又产出字符串?
  • 使用 type Output;:代表「强绑定」。一个具体的异步任务模型,其产出结果类型是唯一确定的。这极大减轻调用者的类型推导心智负担。

4.1.2 Context<'_> 里有什么?(异步的心跳线)

cx(Context)是连接你的代码和底层执行器(Tokio)的唯一通讯管道。它里面最核心的资产叫 Waker(唤醒器)

问题:当 poll 返回 Poll::Pending 时,Tokio 怎么知道什么时候该再次调用 poll?不可能每秒钟盲目轮询一万次。

答案:在返回 Pending 之前,你必须通过 cx.waker().clone() 拿到一个 Waker,并把它注册到操作系统的事件监听器(epoll)里。当网卡数据到达时,底层调用 waker.wake(),Tokio 就会收到信号:「那个任务的数据到了,我应该再去 poll 它一次!」

poll() 调用
  ├── 任务已完成 → 返回 Poll::Ready(结果)
  └── 任务未完成 → ① clone waker 注册到 epoll
                   ② 返回 Poll::Pending
                   ③ ... 等待 OS 通知 ...
                   ④ epoll 触发 waker.wake()
                   ⑤ Tokio 再次 poll()

4.1.3 手工实现 Future(不使用 async/await 语法糖)

一个需要被轮询两次才能完成的计时器:

use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
 
// 1. 定义自己的状态机结构体
struct MyTimer {
    polled_count: u8,
}
 
// 2. 手工实现 Future trait
impl Future for MyTimer {
    type Output = String;  // 最终产出字符串
 
    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        if self.polled_count == 0 {
            println!("第一次轮询,还没准备好...");
            self.polled_count += 1;
 
            // ⚠️ 关键:必须唤醒执行器,否则返回 Pending 后永远沉睡!
            // 真实场景中 waker 会被传给底层 C 网络库或定时器系统
            cx.waker().wake_by_ref();
 
            Poll::Pending  // 告诉 Tokio:还没好,挂起我
        } else {
            println!("第二次轮询,搞定了!");
            Poll::Ready(String::from("任务成功完成!"))
        }
    }
}

关于 Pin<&mut Self>:它是一个内存安全锁。异步编译出的状态机内部会有「自引用」指针,Pin 向编译器发誓:「在这个异步任务彻底执行完之前,不会在内存里移动它的物理位置」,从而防止野指针。

4.2 async/await 编译成了什么?— 状态机 Enum

编译器将 async fn 撕裂重组成一个巨大的隐藏的 Enum(状态机)

// 你写的代码:
async fn fetch_data() -> String {
    let data = reqwest::get("url1").await;  // 断点 ①
    data.text().await                        // 断点 ②
}
 
// 编译器生成的(概念降级后):
enum FetchDataFuture {
    Start,
    WaitingForGet { /* 局部变量 */ },
    WaitingForText { data: Response },
    Finished,
}
 
impl Future for FetchDataFuture {
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<String> {
        loop {
            match *self {
                FetchDataFuture::Start => {
                    // 发起 HTTP 请求,状态切换到 WaitingForGet
                    *self = FetchDataFuture::WaitingForGet { /* ... */ };
                }
                FetchDataFuture::WaitingForGet { .. } => {
                    // 检查网络数据是否就绪
                    // 未就绪 → return Poll::Pending
                    // 已就绪 → 状态切换到 WaitingForText
                }
                FetchDataFuture::WaitingForText { ref data } => {
                    // 读取响应体...
                    // 完成 → return Poll::Ready(result)
                }
                FetchDataFuture::Finished => panic!("已完成的 Future 不应被再次 poll"),
            }
        }
    }
}

每个 .await = 状态机的一个变体。所有局部变量安全保存在 Enum 结构体内部的堆内存中,内存开销极小。

4.3 没有 async/await 之前怎么写异步?

完全可以写,但极其痛苦。 Rust 1.39(2019年)引入 async/await 之前,社区手动实现 Future trait,或使用组合子:

// 没有 async 语法糖的时代 — 回调地狱
fn fetch_and_print() -> impl Future<Item = (), Error = ()> {
    http_get("url1")
        .and_then(|data1| {
            http_get(&format!("url2?param={}", data1))
        })
        .and_then(|data2| {
            println!("{}", data2);
            Ok(())
        })
}

更底层的开发者甚至要手写状态机 Enum,手动在 Poll::PendingPoll::Ready 之间跳转。async/await 就是编译器自动帮你做这件事的语法糖

4.4 async/await 使用要点

// async fn 被调用时,内部代码不会执行,而是立即返回一个 Future
async fn fetch_data() -> String { /* ... */ }
 
let future = fetch_data();  // 什么也没发生,只创建了一个 Future 对象
let result = future.await;  // 真正开始执行,直到完成
 
// 并发执行两个异步任务
let (r1, r2) = tokio::join!(fetch_a(), fetch_b());  // 同时跑
 
// 竞速:谁先回来用谁
tokio::select! {
    result = slow_task() => { println!("慢的先到"); }
    result = fast_task() => { println!("快的先到"); }
    _ = tokio::signal::ctrl_c() => { println!("用户中断"); }
}

五、驱动引擎:Tokio

async/await 只定义了「可以被挂起和恢复的状态机」。谁在底层监控网卡数据?谁推动(Poll)这些状态机向前走?标准库不提供执行器,交给生态。Tokio 就是这个生态的霸主。

5.1 三大核心组件

组件 职责 类比
Executor(执行器) 多线程池,拿着无数 Future,不断调它们的 poll 方法推向下一状态 厂长调度工人
Reactor(反应器) 监听 OS 底层 I/O 事件(epoll/kqueue),网络数据到了就唤醒对应挂起任务,扔回给执行器 物流系统通知材料到了
宏入口 #[tokio::main] 把普通 main 包装进执行器环境 工厂大门

5.2 基本使用

#[tokio::main]   // 过程宏,在幕后生成 Tokio 执行器并 block_on(main)
async fn main() {
    // tokio::spawn 类似 Go 的 go 关键字 — 扔进线程池并发执行
    let handle = tokio::spawn(async {
        let result = fetch_data().await;
        println!("{}", result);
    });
 
    // 等待任务完成
    handle.await.unwrap();
}
 
// 异步 HTTP 请求
async fn fetch_data() -> Result<String, reqwest::Error> {
    let body = reqwest::get("https://httpbin.org/ip")
        .await?
        .text()
        .await?;
    Ok(body)
}

5.3 多层次并发模型

┌─────────────────────────────────────────────┐
│               Tokio Runtime                 │
│  ┌───────────────────────────────────────┐  │
│  │         Executor (线程池)             │  │
│  │  ┌──────┐  ┌──────┐  ┌──────┐       │  │
│  │  │Task1 │  │Task2 │  │Task3 │  ...  │  │
│  │  │ poll │  │ poll │  │ poll │       │  │
│  │  └──┬───┘  └──┬───┘  └──┬───┘       │  │
│  │     │ Pending │ Ready   │ Pending    │  │
│  └──────┼────────┼────────┼─────────────┘  │
│         │        │        │                 │
│  ┌──────┴────────┴────────┴─────────────┐  │
│  │         Reactor (事件监听)            │  │
│  │    epoll/kqueue/IOCP 等待网络IO      │  │
│  └───────────────────────────────────────┘  │
└─────────────────────────────────────────────┘

六、大局观:四层抽象体系

把 Rust 并发系统比作一座现代工厂:

┌──────────────┐  图纸和材料(定义数据形状和状态)
│ Struct/Enum  │  Struct = 数据;Enum = 互斥状态
│   + Impl     │  Impl = 行为挂载
└──────┬───────┘
       │
┌──────┴───────┐  标准化流水线(减少重复代码)
│ 泛型 + 宏    │  泛型 = 类型参数化,零成本单态化
│              │  宏 = AST 级代码生成,编译期展开
└──────┬───────┘
       │
┌──────┴───────┐  工序切分(将耗时任务拆成状态机)
│ async/await  │  async fn → 编译器生成 Future 状态机
│ Future Trait │  Future trait = 统一的轮询接口
└──────┬───────┘
       │
┌──────┴───────┐  厂长调度(极限压榨 CPU)
│    Tokio     │  Executor 调度任务 + Reactor 监听 IO
│              │  多线程 + 非阻塞 = 高并发
└──────────────┘
层级 做什么 发生在何时 对应概念
Struct/Enum 定义数据形状和状态 编码时 图纸
泛型/宏 减少重复、自动生成样板 编译期 标准化流水线
async/await 把顺序逻辑转为状态机 编译期 自动挡变速箱
Future Trait 统一的异步接口契约 编码时定义,运行时使用 标准协议
Tokio 驱动所有状态机不断推进 运行时 发动机

核心公式:async/await = 编译器自动将同步风格的代码,在 AST 层面重写为实现了 Future trait 的状态机 Enum。