rust async 原理

2025/4/201680 字

两周之前学了 Rust圣经 的 这一部分

看了好几遍,才大概懂了些 Rust 的async的异步是怎么实现的。(其它语言的实现方式我想也不会差太多)

首先为什么需要异步,以及为什么不用多线程+回调实现异步我就不多说了。

本篇文章重点就说现代流行的基于协程的异步的实现原理(按照本人目前的理解,大致说明)

一、async 代码实例

先从一个实际使用的 async 编程代码的例子入手,直观理解下异步过程大致的流程。

rust
use std::time::Duration;

async fn sing() {
    println!("start to sing");
    tokio::time::sleep(Duration::from_secs(2)).await;
    println!("finish singing");
}

async fn dance() {
    println!("dance to the music");
}

#[tokio::main]
async fn main() {
    let sing_future = sing();
    let dance_future = dance();
    tokio::join!(sing_future, dance_future);
}
plaintext
start to sing
dance to the music
finish singing

可以看到我们写的两个异步函数似乎是同时进行的,这就是我们使用async异步的原因。

现在让我们关注其背后的原理。

二、背后原理阐释

首先,最让人注意的是,对于两个异步函数的调用,我们并不是直接使用的 await(因为await会阻塞一个异步函数直到其执行完毕,所以如果使用 await 的方式分别调用两个函数,并不能达到这种并发的状态)

而是先获取了两个变量,分别是 sing_future 和 dance_future。这两个变量的类型是实现了 Future 的特征对象,这就涉及到第一个概念——Future。

1. Future

首先,在像使用同步函数一样“调用”被标记为 async 的函数时,并不会发生所谓 “函数的调用”,因为被async标记的看似像函数的东西实际上并不是一个函数,而更接近一个叫做 Future 的类,而对他们的“调用”实际上是创建了对应的 Future 类的对象。

那么 Future 类,准确说是实现了 Future 特征的类 又是什么呢?

我的理解是它一个函数封装起来以便于随时使用的类。

这个类简化长下面的样子

rust
trait SimpleFuture {
    type Output;
    fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}

enum Poll<T> {
    Ready(T),
    Pending,
}

可以看到其关键就是有个方法叫做 poll,顾名思义,使用这个方法,就可以让这个被封装起来的函数开始运行。

那为什么非要封装一下而不是直接调用这个函数呢?

因为我们要实现异步函数,其与同步函数最大的不同就是,它可以选择在执行长等待任务时让出 cpu,从而使得 cpu 可以在其等待时去执行别的任务,实现并发。

这就好像是这个函数可以立即中断它的执行,让出 cpu,并且在合适的时候从中断的地方恢复执行。

要实现上面的功能,就需要这个“函数”有如下的特点:

  1. 可以临时中断–>所以 poll 可以返回两种状态 ready表示完成,pending表示暂停
  2. 可以从中断中恢复执行–>所以需要传入一个 wake() 函数用于唤醒
  3. 中断前后的状态保存–>所以需要用一个类来保存运行时的状态,包括局部变量等

可以看到,这个 Future 类,和线程、进程的概念非常相似,都是一个抽象数据结构,里面有自己运行所需要的状态,以及自己运行所需要的代码。所以使用 Future 这种方式实现异步,运行的单位也被叫做协程(不过实际上协程的实现还要再把 Future 封装成一个叫 Task 的类)。

那么接下来,我们要如何启动这些由 Future 实现的协程呢?

2. 运行 Future 生成器与执行器

这就需要任务生成器和执行器。(这里不讲具体实现,具体实现可以去看 rust 圣经)

类似于生产者于消费者模型,生成器负责接收 Future ,并将其打包创建成 Task,然后把这个 Task 通过消息队列发送给执行器。

执行器就负责不断获取消息队列中的 Task,然后获取里面的 Future,调用其方法 poll,如果返回 ready (任务已完成)就拿走这个 Future 的所有权,销毁掉这个 Future,如果返回 pending 就把这个 Future 放回 Task 里面,不销毁这个 Future。

那没执行完的 Future 怎么办呢,就需要等待其 Task 提供的 wake() 方法被调用,wake() 被调用之后,会把这个 Task 通过生成器放入到消息队列中。

3. wake() 函数的调用者

然后又有疑问了,wake() 函数会被谁来调用呢?

这要看这个 Future 依赖的耗时任务来调用,当一个 Future 启动了一个耗时任务时,或把它自己的 wake() 函数传给其依赖的任务,然后自己就返回 pending 让出 cpu,最后就等它依赖的耗时任务执行完毕后调用这个 wake()。

这个所谓的耗时任务,就我目前的理解,有三种:

  1. 另一个 Future
  2. sleep 这种直接系统调用的等待,需要再开一个线程用来等待,这种不耗cpu,所以开个线程没什么
  3. socket 接收消息的等待,如果采用轮询的方式看消息来没有,单开个线程也耗 cpu 时间,所以需要操作系统支持一种不用耗直接的cpu时间查看 IO事件的机制,这个以后再说(因为现在我也不是很清楚)

关于一个 Future 启动另一个 Future 的方式,这种方式比较特殊,因为被启动的 Future 不是被执行器所启动的,所以返回的 poll 状态也不是交给执行器而是交给调用它的 Future。

通过这种机制,可以实现以下两种特殊的 Future

JoinFuture:并发执行两个 Future,但是只有当两个Future都被执行完毕后才执行完毕

AndThenFuture:也是执行两个Future,不过只有当第一个Future执行完毕后才开始执行第二个Future,相当于同步。

具体怎么实现还是可以去看 rust 圣经

三、代码实例对应的时序图

好,具体大概的原理梳理了一遍。下面贴一张上面提到的示例异步代码运行过程的时序图(这个图实际上才是我梳理的精华)

💬 评论区

写评论

暂无评论,来发表第一条吧。

© 2026 Corn 的自留地