从 Promise 到 Future:Rust 异步的手把手拆解

 谁在跑你的 Rust 异步代码?动手入门

先给你一句话总结:Rust 不帮你跑异步代码,你得自己跑,或者找个跑腿的。

JavaScript 里你写个 await,它自己就跑了。背后有个看不见的引擎(事件循环)在不停转,帮你把 Promise 一个接一个往前推。Rust 正好反过来——语言本身不带任何事件循环。你写个 async fn,它返回一个 Future,然后就……不动了。像个睡着的人,你不戳它,它永远不会醒。

所以问题是:谁来戳它?谁来跑它?

这一章我们就从零搭一个最小的“跑腿的”。你会亲手写一个 Future(就是 Rust 里的 Promise),写一个 Waker(负责喊醒跑腿的人),再写一个叫 block_on 的小引擎(让代码真的跑起来)。等你再看到 Tokio 那种工业级的运行时,会发现里面的东西你全见过了——只是更大、更快、更扛造。



异步函数不跑代码

你肯定写过这种代码(脑子里过一遍就行):

rust
async fn get_user() -> User {
    // 假装从数据库查用户
}
let f = get_user();   // 这行代码跑了吗?

get_user() 被调用了,但里面的代码一行都没执行。它只是造了一个 Future 丢给你,然后就不管了。这个 Future 就是一张欠条,上面写着“以后给你一个 User”。但它不会自己去要账,得有人拿着这张欠条去催。

在 JavaScript 里,Promise 一出生就开始跑了,因为背后有事件循环在撑着。Rust 的 Future 是惰性的,没人催它就永远不动。你想让它动,就得手动去 poll 它。

poll 就是“戳一下”——你问这个 Future:“现在能往前推进吗?”它跑一段,然后告诉你两种情况:
- Ready(值):搞定了,这是你要的结果。
- Pending:还没搞定,我在等某个东西(比如数据库返回、网络响应、定时器到点),你先别催我。

重点来了:没 poll,就没进度。 一个 Future 永远不 poll,就永远不跑一行代码。没有后台引擎,没有隐形的手。全靠你自己。



Poll 一次是不够的

想象你在网上买个东西。下单之后要等卖家发货,等快递送货。你不能站在楼下一秒钟问一次“到了没”——那样太傻了。但也不能问一次就不管了,因为快递确实会到。

异步代码就是这样:你 poll 一次,它没完成(Pending),你就等一会儿,再 poll 一次,还没完成,再等,再 poll……直到某次它给你 Ready。

那等多久?怎么等?这就是 Waker 登场的地方。

poll 函数长这样(先别怕,我们拆开看):

rust
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll

这里有三个陌生东西:Pin<&mut Self>ContextPoll。我们一个一个说。

Poll 最直白:它就是 Ready(T)Pending 二选一。

Pin<&mut Self> 是因为 Future 内部可能指着自己。比如它存了一个数据库连接,同时存了一个指向这个连接的引用。如果你把这个 Future 移动到内存别的地方,那个引用就成野指针了。Pin 就是一种承诺:“这个 Future 在 poll 期间不会移动”。现在你只需要知道:poll 需要这个。

Context 里面藏着一个 Waker。Waker 就是“喊醒你”的工具。流程是:
1. 你(跑腿的)调用 poll,给 Future 传一个 Waker。
2. Future 发现自己要等数据库,就把这个 Waker 交给数据库连接:“数据到了就喊我”。
3. 然后返回 Pending
4. 你(跑腿的)去睡觉(比如把当前线程挂起)。
5. 数据库回来了,它调用 Waker 的 wake() 方法。
6. 你被喊醒,再次 poll 这个 Future,这次它就能拿出数据返回 Ready

Waker 就是一个“门铃”。你给 Future 一个门铃,Future 把它贴在墙上等,自己去睡觉。门铃响了再起来干活。



做一个一次性通道

把这些概念串起来的最好办法,是自己写一个 oneshot channel。这是一个只传一次值、只给一个人用的通道。发送端把值丢进去,接收端等着拿。跟 Promise 很像:你 resolve 一个值,然后 await 拿到它。

我们分步骤来。创建项目:

bash
cargo new oneshot
cd oneshot

先定义两个结构体:SenderReceiver。它们要共享同一块内存,因为发过去的值得存到一个地方,接收方才能读到。这块共享内存叫 Inner

rust
use std::future::Future;
use std::pin::{pin, Pin};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};

struct Sender {
    inner: Arc>
}

struct Receiver {
    inner: Arc>
}

struct Inner {
    value: Option,
    waker: Option,
}

为什么套这么多层?ArcSenderReceiver 可以共享同一个 Inner(引用计数)。Mutex 保证同一时刻只有一个人能改 Inner 里面的东西,免得发送方和接收方同时写,把数据写烂了。

构造 oneshot 函数:创建一个 Inner,包上 Arc,再克隆一份给 SenderReceiver

rust
fn oneshot() -> (Sender, Receiver) {
    let inner = Arc::new(Mutex::new(Inner {
        value: None,
        waker: None,
    }));
    (Sender { inner: inner.clone() }, Receiver { inner })
}

发送:把值放进去,如果有 Waker 就喊醒它。

rust
impl Sender {
    fn send(self, value: String) {
        let mut inner = self.inner.lock().unwrap();
        inner.value = Some(value);
        if let Some(waker) = inner.waker.take() {
            waker.wake();
        }
    }
}

注意 send 拿的是 self 而不是 &self。oneshot 只发一次,发完 Sender 就被消费掉了,第二次调 send 编译都过不了。

接收:实现 Futurepoll 里锁住 Inner,看值到了没。到了就 Ready,没到就把传进来的 Waker 存起来,返回 Pending

rust
impl Future for Receiver {
    type Output = String;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
        let mut inner = self.inner.lock().unwrap();
        if let Some(value) = inner.value.take() {
            Poll::Ready(value)
        } else {
            inner.waker = Some(cx.waker().clone());
            Poll::Pending
        }
    }
}

现在 Receiver 是一个 Future,但它不会自己跑。需要一个“跑腿的”来 poll 它。



造一个最小的跑腿的:block_on

block_on 的意思是:阻塞当前线程,直到这个 Future 完成。它是同步世界和异步世界之间的那扇门。

思路很简单:
- 把 Future 钉住(pin!)。
- 造一个 Waker,这个 Waker 能喊醒当前线程。
- 循环:poll 一次,如果 Ready 就返回值,如果 Pending 就挂起线程(thread::park()),等着被喊醒。

先实现 Waker。Rust 标准库有个 Wake trait,你只需要实现 wake 方法。我们让 wake 里调用 unpark()——线程的“起床铃”。

rust
use std::task::Wake;
use std::thread::Thread;

struct ThreadWaker(Thread);

impl Wake for ThreadWaker {
    fn wake(self: Arc) {
        self.0.unpark();
    }
}

然后写 block_on

rust
fn block_on(future: F) -> F::Output {
    let mut future = pin!(future);
    let waker = Waker::from(Arc::new(ThreadWaker(thread::current())));
    let mut cx = Context::from_waker(&waker);
    loop {
        match future.as_mut().poll(&mut cx) {
            Poll::Ready(value) => return value,
            Poll::Pending => thread::park(),
        }
    }
}

流程:
1. 钉住 Future(因为 poll 需要 Pin)。
2. 拿到当前线程的句柄,包成 ThreadWaker,再转成真正的 Waker
3. 用这个 Waker 造一个 Context
4. 循环:poll,如果 Ready 就返回;如果 Pendingpark()(线程睡觉),直到被 unpark() 喊醒,然后继续循环。



全部拼起来跑一次

现在把发送端放到另一个线程里,假装是一个慢数据库查询。主线程用 block_on 等结果。

rust
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = oneshot();

    thread::spawn(move || {
        thread::sleep(Duration::from_millis(500));
        tx.send("a fresh database row".to_string());
    });

    let row = block_on(rx);
    println!("{row}");
}

move 关键字让闭包拿走 tx 的所有权,不然新线程还在跑,主线程可能已经结束,tx 就失效了。

运行:

bash
cargo run

大概半秒后输出:


a fresh database row

你刚亲手跑起来一个 Future。没有 Tokio,没有 async-std,只有 std。你写了 Receiver(它是个 Future),你写了 Waker(用线程挂起/唤醒),你写了 block_on(一个小型运行时)。



下一章:从跑一个到跑一堆

你现在知道怎么跑一个 Future 了。但真实场景里,一个线程要同时跑几百上千个 Future——比如一个 Web 服务器同时处理几千个请求。你不能给每个请求开一个线程,线程太贵了。你得有一个执行器(executor),在一个线程上轮询很多 Future:这个 Pending 就换下一个,那个 Ready 就收结果。

下一章你会亲手写一个这样的执行器。你会看到所谓的“异步”就是在单线程上快速切换任务,每个任务只在有进展的时候才占用 CPU。你也会看到 Waker 的真实威力:它让执行器可以安心睡觉,只有某个 Future 就绪时才起来干活。

你还会搞懂 Pin 到底在防什么——为什么 Future 内部会指向自己,以及 Rust 怎么保证你不会踩到那个坑。

每一章你都会先自己造一个轮子,然后再去用 Tokio 的现成版本。等你看到 tokio::spawntokio::sync::oneshottokio::task::yield_now 的时候,你会说:“哦,这玩意儿我自己写过。”



总结

本文从零解释 Rust 异步的核心机制。手写 Future、Waker、block_on 执行器,用 oneshot channel 串联所有概念。不依赖任何运行时,只用标准库。适合已经会 JavaScript 异步、想真正理解 Rust async 如何工作的读者。


你现在手里有一个能跑的异步通道:发送线程把数据丢进去,接收线程用 block_on 等着拿。pollWakerPendingReady 这些词不再是天书——你亲手写过它们。