Rust 技术笔记
七月 18, 2025 [rust] #rust #async #future #tokio #pin #gdbRust 技术笔记
异步编程核心概念
async/await 与 Future
Rust 的异步模型与其他语言的关键差异:Future 是惰性的,只有被 .await 或 executor 轮询时才执行。JavaScript 的 Promise 一旦创建就开始执行,Rust 的 Future 不会被轮询就什么都不做。
async fn 和 async {} 是语法糖,返回一个实现了 Future trait 的状态机。编译器会将其转换为 GenFuture 结构。
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
pub enum Poll<T> {
Ready(T),
Pending,
}
异步 vs 多线程
| 同步 | 多线程 | async/await | |
|---|---|---|---|
| IO 等待时间 | time1 + time2 | max(time1, time2) | max(time1, time2) |
| 资源开销 | 最低 | 高(每任务一线程) | 低(协程) |
| 适用场景 | 简单脚本 | CPU 密集型 | IO 密集型 |
// 异步并发读取两个文件
let (content1, content2) = try_join!(
fs::read_to_string("./Cargo.toml"),
fs::read_to_string("./Cargo.lock"),
)?;
Executor 调度器
Rust 在语言层面不提供 executor,需要在生态中选择:
- tokio — 最主流的异步运行时
- async-std — API 贴近标准库
- smol — 轻量级
- futures — 官方试验性运行时
Reactor Pattern:Executor 维护就绪队列和等待队列,Reactor 利用 epoll/kqueue/IOCP 监听 IO 事件,通过 Waker.wake() 唤醒挂起的 Future。Tokio 采用 work-stealing 多线程调度。
协作式调度的注意事项
- 避免在异步任务中执行计算密集型操作——会饿死其他任务
- CPU 密集任务用线程池,通过 channel 与异步任务通信
- 必要时使用
tokio::task::yield_now()主动让出 CPU
// 线程与异步任务的 channel 通信
let (sender, mut receiver) = mpsc::unbounded_channel::<(String, oneshot::Sender<String>)>();
thread::spawn(move || {
while let Some((line, reply)) = receiver.blocking_recv() {
let result = compute_heavy(&line);
reply.send(result).ok();
}
});
// 在异步任务中:
sender.send((data, reply))?;
let result = reply_receiver.await?;
async 生命周期
async fn 返回的 Future 生命周期受限于引用参数:
async fn borrow_x(x: &u8) -> u8 { *x }
// 等价于
fn borrow_x<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
async move { *x }
}
解决引用生命周期问题:将参数和 async 调用放在同一个 async 块内:
fn good() -> impl Future<Output = u8> {
async {
let x = 5;
borrow_x(&x).await
}
}
async move
- 不
move:多个 async 块可共享同一个局部变量(在作用域内) - 使用
move:所有权转移,消除借用生命周期约束
异步代码中的 Mutex
标准库的 MutexGuard 不能跨越 .await,必须使用异步友好的 tokio::sync::Mutex:
let db = Arc::new(tokio::sync::Mutex::new(DB));
let mut guard = db.lock().await;
guard.commit().await?; // std::sync::Mutex 这里会编译失败
Pin 与 Unpin
为什么需要 Pin
async 函数编译后生成自引用结构——Future 内部的字段可能引用同一结构中的其他字段。如果这个结构被移动(move/swap),自引用指针就会指向旧地址,导致内存安全问题。
Pin 保证被 Pin 住的值不会再被移动。
Unpin
- 大多数类型自动实现
Unpin,表示可以安全移动 async生成的 Future 默认是!Unpin的- 添加
PhantomPinned字段可以显式声明!Unpin
实践
use std::pin::Pin;
use std::marker::PhantomPinned;
// 固定到栈上(unsafe)
let mut val = MyStruct::new();
let mut pinned = unsafe { Pin::new_unchecked(&mut val) };
// 固定到堆上(safe)
let boxed: Pin<Box<MyStruct>> = Box::pin(val);
// 用 pin_mut! 宏固定栈变量
pin_utils::pin_mut!(fut);
当需要 Future 实现 Unpin 时,使用 Box::pin() 或 pin_mut!() 将其固定。
Stream trait
Stream 类似异步版的 Iterator,可以多次 yield 值:
pub trait Stream {
type Item;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>>;
}
常用方法在 StreamExt trait 中(需引入 futures crate)。
Tokio 常用模式
Scoped Task
tokio_scoped::scope(|scope| {
scope.spawn(async {
v.push('!');
});
});
assert_eq!(v.as_str(), "Hello!");
FuturesUnordered
并发执行多个 Future,按完成顺序获取结果:
let mut workers = FuturesUnordered::new();
for i in 0..5 {
workers.push(do_work(i));
}
while let Some(result) = workers.next().await {
// 处理结果
}
join_all
let results: Vec<String> = join_all(
urls.iter().map(|url| fetch(url))
).await;
常用代码片段
从 Vec 中同时获取多个可变引用
let mut v = vec![String::from("abc"), String::from("def")];
if let [s0, s1] = &mut v[..] {
s0.push_str(s1);
}
HRTB(高阶 trait 约束)
fn foo(b: Box<dyn DoSomething<&usize>>, s: &usize) {
b.do_sth(s);
}
使用 trait 方法必须引入 trait
use futures::StreamExt; // 不同 crate 的 trait 可用 as 别名
GDB 调试 Rust
版本匹配
rustup default 1.82.0 # 确保编译版本与调试版本一致
调用 trait 方法限制
GDB 无法直接调用 trait 方法(编译器未生成完整 trait 调试信息):
>>> print path.is_absolute()
Could not find function named 'std::path::Path::is_absolute'