Rust async原理剖析

上篇文章异步编程async中提到了Rust的async使用方式,今天这篇文章就来进一步的介绍下在Rust中async是怎么实现的,通过其实现原理来帮助我们更好的在实践中使用其特性。

  • 前文中Rust的异步编程是通过async的方式实现的,其实在其标准库中也可以通过多线程来实现,只是其在一些场景上不太合适,比如网络爬虫,所以并没有过多的介绍,如果感兴趣的话可以自行查阅。

Rust async/await简介

async/awaitRust中内置的语法,类似Golang中的go语法,可以让我们用同步的方式去编写异步逻辑代码。

但是它和其他语言的实现还是有所区别的,主要有如下几点:

  • async在Rust中使用时是零成本(zero-cost)。这就是说你能看到的代码(也就是你自己写的业务代码)才有性能损耗,你看不到的代码(也就是async的内部实现)是没有性能损耗的。你不会为使用async而多付出隐藏的性能成本
  • Rust中没有内置的异步调用所需的运行时,需要Rust社区生态对其进行提供,目前使用较多的是tokioasync-std
  • 运行时同时支持多线程和单线程。

async在Rust中的底层实现较为复杂,本篇文章主要介绍其在实现过程中的一些关键原理。async被认为是一个语法糖,Rust编译async标记的语法块时会将其转换成一个实现了Future特征的状态机,然后由异步调用所需的运行时中的某个线程去执行,当某个Future被阻塞,则让出当前线程的控制权,这样其它的Future就可以在该线程中执行,这就可以在不阻塞当前线程的情况下实现异步调用。

所以async/await在正常使用时需要依赖以下内容:

  • 由标准库提供的所必须的关键字、类型、函数和特征Future
  • 在编译器层面支持对关键字async/await支持。
  • 由社区开发的async/await运行时来提供async代码的执行、IO操作、任务创建和调度功能。

我们来看个简单的demo感受下吧,

  • 这里使用的运行时是tokio,需要在Cargo.toml中添加相应的依赖,如下:
# [dependencies]
tokio = { version = "1.45.0", features = ["full"] }Code language: PHP (php)

main.rs中的代码如下:

use tokio::time::{sleep, Duration};

async fn eat_dish() {
    println!("eating dinner.");
    sleep(Duration::from_secs(1)).await;
}

#[tokio::main]
async fn main() {
    eat_dish().await;
    println!("Finished!");
}Code language: PHP (php)

原理剖析

代码使用了asyncawaittokio来实现一个异步的功能。

首先async fn eat_dish被编译器编译成了实现了Future特征的状态机,源码中对Future的定义如下:

pub trait Future {
    type Output;

    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}Code language: HTML, XML (xml)

那么async fn eat_dish被编译之后的伪代码如下:

// async fn eat_dish() is desugared into:
struct EatDishFuture {
    state: State, // states of a manually implemented state machine
}

impl Future for EatDishFuture {
    type Output = (); // associated type

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<()> {
        match self.state {
            State::Start => {
                println!("eating dinner.");
                let sleep_fut = sleep(Duration::from_secs(1));
                self.state = State::Sleeping(sleep_fut);
                Poll::Pending
            }

            State::Sleeping(ref mut sleep_fut) => {
                // sleep_fut returning Ready upon polling indicates the timer has finished.
                match Pin::new(sleep_fut).poll(cx) {
                    Poll::Pending => Poll::Pending,
                    Poll::Ready(()) => Poll::Ready(()),
                }
            }
        }
    }
}Code language: PHP (php)

其次await被编译器转换成对poll()的调用和状态管理需要注意的是await方法只能在async的方法中被调用

代码的main方法上添加了#[tokio::main]宏,它的作用是启动一个tokio运行时,并启动任务调度器和运行main()的异步状态机。

整个代码的执行逻辑是tokio运行时调用async main,然后遇到eat_dish().await变成调用EatDishFuture.poll,而eat_dish()中有一个sleep来模拟执行业务代码的耗时,所以EatDishFuture被阻塞挂起,主线程可以调用其他的Future,不过这里只有一个EatDishFuture,接下来主线程循环调用所有挂起的Futurepoll,随后EatDishFuture执行结束之后,调用async main中的println!("吃完了!");,最后结束进行主进程。整个过程是零堆分配、无阻塞、完全基于 poll 来驱动的

自定义EatDishFuture

上文中提到Rust编译器会将async fn eat_dish编译成一个继承自Future的结构体,那下面我们直接自己实现下这个Future结构体,看下运行效果,代码如下:

use std::pin::Pin;
use std::task::{Context, Poll};
use std::future::Future;
use tokio::time::{sleep, Duration, Sleep};

enum State {
    Start,
    Sleeping(Pin<Box<Sleep>>),
    Done,
}

struct EatDish {
    state: State,
}

impl EatDish {
    fn new() -> Self {
        Self {
            state: State::Start,
        }
    }
}

impl Future for EatDish {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
        loop {
            match &mut self.state {
                State::Start => {
                    println!("eating dinner.");
                    let sleep_future = Box::pin(sleep(Duration::from_secs(1)));
                    self.state = State::Sleeping(sleep_future);
                }
                State::Sleeping(fut) => {
                    // manually poll the sleep future
                    match fut.as_mut().poll(cx) {
                        Poll::Pending => return Poll::Pending,
                        Poll::Ready(()) => {
                            self.state = State::Done;
                        }
                    }
                }
                State::Done => {
                    return Poll::Ready(());
                }
            }
        }
    }
}

#[tokio::main]
async fn main() {
    EatDish::new().await;
    println!("Finished!");
}Code language: PHP (php)

其运行的效果与之前效果的一样,这里我们自定义了一个EatDish的结构体,并实现了Future特征,利用状态机来实现运行状态的变化,这样我们达到了async fn eat_dish的功能,最后在tokio::main中调用EatDish::new().await就可以实现异步了。

总结

Rust的async/await异步编程是基于Future特征,将async fn语句块编译成继承自Future的结构体,这种优雅的方式结合了强大的类型系统和零成本抽象,使其实现了高性能且安全的异步编程模型。

这种通过将async fn编译为状态机,并由异步运行时驱动poll执行的设计模式,使Rust实现了无需阻塞线程的高效异步调度模型。虽然与其它语言的异步模型相比,Rust的实现更加底层和复杂,但是这也给Rust带来了更大的性能优势和灵活度。

在实际的开发中,由于Rust本身并没有提供async/await异步所需的运行时,所以需要借助社区提供的运行时(比如tokioasync-std)来简化异步任务的调度管理和执行。如果能够理解其底层原理不仅有助于更高效地使用async/await,也能在遇到性能瓶颈或调试复杂异步流程时提供一些解决思路。

这篇文章在异步编程async的基础之上,对Rust的async异步编程模式从原理的角度进行了剖析,在后续的文章中我们将继续介绍async在一些具体的场景中如何发挥其巨大的作用的。