Rust Async 异步编程(四):Executor
Rust Async 异步编程(四):Executor
异步编程背后到底藏有什么秘密?究竟是哪只幕后之手在操纵这一切?如果你对这些感兴趣,就继续看下去,否则可以直接跳过,因为本章节的内容对于一个 API 工程师并没有太多帮助。
但是如果你希望能深入理解 Rust 的 async/.await 代码是如何工作、理解运行时和性能,甚至未来想要构建自己的 async 运行时或相关工具,那么本章节终究不会辜负于你。
执行器
Rust 的 Future 是惰性的:只有屁股上拍一拍,它才会努力动一动。其中一个推动它的方式就是在 async 函数中使用 .await 来调用另一个 async 函数,但是这个只能解决 async 内部的问题,那么这些最外层的 async 函数,谁来推动它们运行呢?答案就是我们之前多次提到的执行器 executor。
执行器会管理一批 Future (最外层的 async 函数),然后通过不停地 poll 推动它们直到完成。 最开始,执行器会先 poll 一次 Future,后面就不会主动去 poll 了,而是等待 Future 通过调用 wake 函数来通知它可以继续,它才会继续去 poll。这种 wake 通知然后 poll 的方式会不断重复,直到 Future 完成。
构建执行器
下面我们将实现一个简单的执行器,它可以同时并发运行多个 Future。
例子中,需要用到 futures 包的 ArcWake trait,它可以提供一个方便的途径去构建一个 Waker。编辑 Cargo.toml ,添加下面依赖:
[dependencies]
futures = "0.3"
在之前的内容中,我们在 src/lib.rs 中创建了定时器 Future ,现在在 src/main.rs 中来创建程序的主体内容,开始之前,先引入所需的包:
use {
futures::{
future::{BoxFuture, FutureExt},
task::{waker_ref, ArcWake},
},
std::{
future::Future,
sync::mpsc::{sync_channel, Receiver, SyncSender},
sync::{Arc, Mutex},
task::{Context, Poll},
time::Duration,
},
// 引入之前实现的定时器模块
timer_future::TimerFuture,
};
执行器需要从一个消息通道(channel)中拉取事件,然后运行它们。当一个任务准备好后(可以继续执行),它会将自己放入消息通道中,然后等待执行器 poll。
/// 任务执行器,负责从通道中接收任务然后执行
struct Executor {
ready_queue: Receiver<Arc<Task>>,
}
/// Spawner 负责创建新的 Future 然后将它发送到任务通道中
#[derive(Clone)]
struct Spawner {
task_sender: SyncSender<Arc<Task>>,
}
/// 一个 Future,它可以调度自己(将自己放入任务通道中),然后等待执行器去 poll
struct Task {
/// 进行中的 Future,在未来的某个时间点会被完成
///
/// 按理来说 Mutex 在这里是多余的,因为我们只有一个线程来执行任务。但是由于
/// Rust 并不聪明,它无法知道 Future 只会在一个线程内被修改,并不会被跨线程修改。
/// 因此我们需要使用 Mutex 来满足这个笨笨的编译器对线程安全的执着。
///
/// 如果是生产级的执行器实现,不会使用 Mutex,因为会带来性能上的开销,取而代之的是使用 UnsafeCell
future: Mutex<Option<BoxFuture<'static, ()>>>,
/// 可以将该任务自身放回到任务通道中,等待执行器的 poll
task_sender: SyncSender<Arc<Task>>,
}
fn new_executor_and_spawner() -> (Executor, Spawner) {
// 任务通道允许的最大缓冲数(任务队列的最大长度)
// 当前的实现仅仅是为了简单,在实际的执行中,并不会这么使用
const MAX_QUEUED_TASKS: usize = 10_000;
let (task_sender, ready_queue) = sync_channel(MAX_QUEUED_TASKS);
(Executor { ready_queue }, Spawner { task_sender })
}
下面再来添加一个方法用于生成 Future,然后将它放入任务通道中:
impl Spawner {
fn spawn(&self, future: impl Future<Output = ()> + 'static + Send) {
let future = future.boxed();
let task = Arc::new(Task {
future: Mutex::new(Some(future)),
task_sender: self.task_sender.clone(),
});
self.task_sender.send(task).expect("任务队列已满");
}
}
在执行器 poll 一个 Future 之前,首先需要调用 wake 方法进行唤醒,然后再由 Waker 负责调度该任务并将其放入任务通道中。创建 Waker 的最简单的方式就是实现 ArcWake 特征,先来为我们的任务实现 ArcWake trait,这样它们就能被转变成 Waker 然后被唤醒:
impl ArcWake for Task {
fn wake_by_ref(arc_self: &Arc<Self>) {
// 通过发送任务到任务管道的方式来实现 wake,这样 wake 后,任务就能被执行器 poll
let cloned = arc_self.clone();
arc_self
.task_sender
.send(cloned)
.expect("任务队列已满");
}
}
当任务实现了 ArcWake trait 后,它就变成了 Waker ,在调用 wake() 对其唤醒后会将任务复制一份所有权(Arc),然后将其发送到任务通道中。最后我们的执行器将从通道中获取任务,然后进行 poll 执行:
impl Executor {
fn run(&self) {
while let Ok(task) = self.ready_queue.recv() {
// 获取一个 future,若它还没有完成,则对它进行一次 poll 并尝试完成它
let mut future_slot = task.future.lock().unwrap();
if let Some(mut future) = future_slot.take() {
// 基于任务自身创建一个 LocalWaker
let waker = waker_ref(&task);
let context = &mut Context::from_waker(&*waker);
// BoxFuture<T> 是 Pin<Box<dyn Future<Output = T> + Send + 'static>> 的类型别名
// 通过调用 as_mut 方法,可以将上面的类型转换成 Pin<&mut dyn Future + Send + 'static>
if future.as_mut().poll(context).is_pending() {
// Future 还没执行完,因此将它放回任务中,等待下次被 poll
*future_slot = Some(future);
}
}
}
}
}
恭喜!我们终于拥有了自己的执行器,下面再来写一段代码使用该执行器去运行之前的定时器 Future:
fn main() {
let (executor, spawner) = new_executor_and_spawner();
// 生成一个任务
spawner.spawn(async {
println!("howdy!");
// 创建定时器 Future,并等待它完成
TimerFuture::new(Duration::new(2, 0)).await;
println!("done!");
});
// drop 掉任务,这样执行器就知道任务已经完成,不会再有新的任务进来
drop(spawner);
// 运行执行器直到任务队列为空
// 任务运行后,会先打印 howdy!, 暂停 2 秒,接着打印 done!
executor.run();
}
图解程序执行流程
-
调用 new_executor_and_spawner() 函数,创建 spawner 和 executor。
-
调用 spawner.spawn() 函数,生成一个任务,发送到通道中。

- 调用 executor.run() 函数,取出通道中的任务,调用 poll() 方法开始执行 Future。

- Future 是一个 async 块,TimerFuture 创建后,执行它的 poll() 方法。

- 前一步的 poll() 方法返回 Poll::Pending,executor 将 Future 放回任务中,等待下次被 poll。

- 在 TimerFuture 创建的新线程中,先休眠 2 秒,将 shared_state.completed 设为 true,获取 waker,调用 wake() 方法进行唤醒,于是 Task 的 wake_by_ref() 方法被调用(ArcWake trait),把 Task 放回通道中。

-
executor.run() 函数再次取出通道中的任务,调用 poll() 方法开始执行 Future。
-
Future 是一个 async 块,再次执行它的 poll() 方法,这次返回 Poll::Ready,说明 Future 执行完毕。
-
executor.run() 函数发现通道内没有任务了,程序退出。

参考
- https://github.com/rustcn-org/async-book
- https://www.bilibili.com/video/BV1Ki4y1C7gj
2万+

被折叠的 条评论
为什么被折叠?



