Rust async原理剖析
上篇文章异步编程async中提到了Rust的async
使用方式,今天这篇文章就来进一步的介绍下在Rust中async是怎么实现的,通过其实现原理来帮助我们更好的在实践中使用其特性。
- 前文中Rust的异步编程是通过
async
的方式实现的,其实在其标准库中也可以通过多线程来实现,只是其在一些场景上不太合适,比如网络爬虫,所以并没有过多的介绍,如果感兴趣的话可以自行查阅。
Rust async/await简介
async/await
是Rust
中内置的语法,类似Golang
中的go
语法,可以让我们用同步的方式去编写异步逻辑代码。
但是它和其他语言的实现还是有所区别的,主要有如下几点:
async
在Rust中使用时是零成本(zero-cost)。这就是说你能看到的代码(也就是你自己写的业务代码)才有性能损耗,你看不到的代码(也就是async
的内部实现)是没有性能损耗的。你不会为使用async
而多付出隐藏的性能成本。- Rust中没有内置的异步调用所需的运行时,需要Rust社区生态对其进行提供,目前使用较多的是
tokio
和async-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)
原理剖析
代码使用了async
、await
和tokio
来实现一个异步的功能。
首先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
,接下来主线程循环调用所有挂起的Future
的poll
,随后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
异步所需的运行时,所以需要借助社区提供的运行时(比如tokio
和async-std
)来简化异步任务的调度管理和执行。如果能够理解其底层原理不仅有助于更高效地使用async/await
,也能在遇到性能瓶颈或调试复杂异步流程时提供一些解决思路。
这篇文章在异步编程async的基础之上,对Rust的async
异步编程模式从原理的角度进行了剖析,在后续的文章中我们将继续介绍async
在一些具体的场景中如何发挥其巨大的作用的。