Rust 核心概念深度拆解
Rust 核心概念深度拆解
Rust 的设计像一组精密齿轮:数据建模(Struct/Enum/Impl)和抽象表达(泛型/宏)是底层物理齿轮,异步并发(Async/Tokio)是建立其上的高阶引擎。
最核心的结构性联系:async/await 在编译期,底层被转换成带数据的 Enum(状态机)。
在内存层面,struct 和 enum 都是用来描述“数据集合”的工具,只是组合逻辑不同:
-
struct是积类型(Product Type / AND): 一个User结构体包含name(字符串)并且(AND) 包含age(整数)。它在内存中把这几块空间拼接到一起。 -
enum是和类型(Sum Type / OR): 一个Message枚举可以是Quit(无数据),或者(OR) 可以是Move {x: i32, y: i32}(包含坐标数据),或者 可以是Write(String)(包含字符串)。 -
struct和enum负责定义内存中的数据形态(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 |
异构集合、插件系统 | 虚函数表查找 |
推荐学习顺序:
- 掌握
match+ 带数据 enum - 为 enum 实现标准库 Trait(
Debug、Display、Clone) - 用 enum + 自定义 Trait 实现领域模型
- 深入
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 不是魔法,它是帮你把 From、Display、Error 三个 Trait 的样板代码自动生成的构建工具。
2.5.5 更深一层:? 不是编译器硬编码的
上面说 ? 展开为 match + From::from,但这是简化版。真正的底层机制更加解耦:Rust 不是靠编译器对 Result 或 Option 的「特殊照顾」来判断的,而是把决定权完全交给了类型系统。
终极裁判:ControlFlow 枚举
pub enum ControlFlow<B, C> {
Continue(C), // 一切正常,继续执行,携带继续的值
Break(B), // 出状况了,立刻中断并返回这个错误/空值
}无论你是什么类型(Result 也好,Option 也好),只要想用 ?,就必须把自己的状态「翻译」成 ControlFlow 的 Continue 或 Break。编译器只认 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——? 是一把专门「开箱」的钥匙,只有面对 Result 或 Option 这种「薛定谔的盲盒」时才需要且能够使用。
三、代码生成器:宏(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.rs 或 src/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 数据结构及其所有子节点。宏可以在内存里:
- 读取子节点的名字(如
"add"),改成"add_async_version" - 在函数体的最外层嫁接一层
tokio::runtime::Builder::new()...的树枝 - 添加或删除参数、插入新的语句
这解释了为什么 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 MyTask又impl 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::Pending 和 Poll::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。