Rust 技术笔记

七月 18, 2025 [rust] #rust #async #future #tokio #pin #gdb

Rust 技术笔记

异步编程核心概念

async/await 与 Future

Rust 的异步模型与其他语言的关键差异:Future 是惰性的,只有被 .await 或 executor 轮询时才执行。JavaScript 的 Promise 一旦创建就开始执行,Rust 的 Future 不会被轮询就什么都不做。

async fnasync {} 是语法糖,返回一个实现了 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 + time2max(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,需要在生态中选择:

Reactor Pattern:Executor 维护就绪队列和等待队列,Reactor 利用 epoll/kqueue/IOCP 监听 IO 事件,通过 Waker.wake() 唤醒挂起的 Future。Tokio 采用 work-stealing 多线程调度。

协作式调度的注意事项

  1. 避免在异步任务中执行计算密集型操作——会饿死其他任务
  2. CPU 密集任务用线程池,通过 channel 与异步任务通信
  3. 必要时使用 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

异步代码中的 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

实践

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'

相关 issue:Rust #1563#43334