一、Rust 异步调度底层原理
在Rust异步编程中,有一个async
关键字,async
修饰的函数具备异步非阻塞的执行能力,原理是async
修饰的函数具备了Future
特征,而Future
是Rust异步调度的关键单元,这篇文章主要是讨论Rust异步编程底层调度的原理。
1、Rust调度模型设计
在讨论底层如何进行调度之前,我们先明确Rust的调度模型是怎么样的。
Future
相当于一个函数,在调度系统中可以理解成一个任务,这个任务是运行在某个线程上的。
在Golang
的GMP
调度模型中,通过go
关键字,可以创建个协程G
,协程G
和OS的内核级线程
是M:N的关系。
在Rust
的Future
调度模型中,通过async
关键字,可以创建多个Future
,在一个线程上创建的多个Future
,Future
和该线程
的关系是N:1的关系,
修正:如果执行器是多线程的,Future
和该线程
的关系可能是M:N的关系
非常关键!!!!
Rust Future
的执行是被执行器poll(轮询)后才能运行,poll
其实是Future特征的一个函数,当轮到这个Future执行时,执行器会去执行这个future的poll函数,推进Future的前进,我们可以看一段代码。
pub struct SocketRead<'a> {
socket: &'a Socket,
}
impl SimpleFuture for SocketRead<'_> {
type Output = Vec<u8>;
fn poll(&mut self, wake: fn()) -> Poll<Self::Output> {
if self.socket.has_data_to_read() {
// socket有数据,写入buffer中并返回
Poll::Ready(self.socket.read_buf())
} else {
// socket中还没数据
//
// 注册一个`wake`函数,当数据可用时,该函数会被调用,
// 然后当前Future的执行器会再次调用`poll`方法,此时就可以读取到数据
self.socket.set_readable_callback(wake);
Poll::Pending
}
}
}
2、Rust调度模型详细过程
先说结果,我们再通过代码中验证我们的结果。有点像Epoll,执行器类似epoll中的操作系统调度,Future是监听事件,执行器不需要全部逐个检查Future是否能执行,而是类似future通知执行器,我可以执行。通知是通过waker来通知的。
- 创建一个管道任务队列,用来存放Task任务(Future),sender,receiver。sender给spawner用于发送任务、receiver给executor接收任务并执行。
- spawner::spawn方法调用,创建一个Future(async函数),并将这个Future包裹为一个Task,这个Task克隆了一份spawner的发送管道引用(其实还是用那个管道,但是有两个入口了)。Spawner将task发送进管道
- Executor会死循环检查管道任务队列,如果管道队列中有任务,取出任务future任务不是空的 => 给Future任务创建一个Waker
- Poll这个Future,如果结果是Pending, 那么将这个future放回Task的future槽。
- 如果不是Pending那么该任务结束。
- 该任务线程会在准备继续的时候调用第3步给他创建的Waker,并将自己放回任务管道。
- 再次被Executor接收到,取出Future,创建waker,poll执行…循环4~6
角色大概可以分成几种:Future
(Task)、执行器
(负责执行Future)、任务队列
(存放可以执行,但是等待执行的Future)、Waker
(Future不阻塞后,通过Waker将自己放进任务队列中)
步骤一
,创建一个管道任务队列,并返回一个接收者、发送者,我们可以直接引入包mpsc
,当然还包括其他
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,
};
步骤二
主要是讲Future(Task)如何放到任务管道中的,这个我们后面再说
步骤三
提到一个执行器,我们来看看执行器构建的代码
/// 任务执行器,负责从通道中接收任务然后执行
struct Executor {
ready_queue: Receiver<Arc<Task>>,
}
/// `Spawner`负责创建新的`Future`然后将它发送到任务通道中
#[derive(Clone)]
struct Spawner {
task_sender: SyncSender<Arc<Task>>,
}
/// 一个Future,它可以调度自己(将自己放入任务通道中),然后等待执行器去`poll`
struct Task {
/// 包着一个进行中的Future,在未来的某个时间点会被完成
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;
// task_sender::发送器,给Spawner
//ready_queue管道任务队列接收器,给执行器
let (task_sender, ready_queue) = sync_channel(MAX_QUEUED_TASKS);
(Executor { ready_queue }, Spawner { task_sender })
}
步骤三
还提到执行器死循环检查管道任务队列,我们来看看执行器运行的代码
impl Executor {
fn run(&self) {
while let Ok(task) = self.ready_queue.recv() {//读取管道任务
// 获取一个future,若它还没有完成(仍然是Some,不是None),则对它进行一次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);
}
}
}
}
}
步骤四
步骤三代码用到一个let waker = waker_ref(&task)
;,正好可以弥补4说的,如果future
结果是Pending
,给Future
任务创建一个Waker
,将这个future放回Task的future槽,等future
阻塞结束,调用wake
函数,future又放回管道任务队列中,等待执行器再次执行该future
的poll
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("任务队列已满");
}
}
到此结束了哈哈,写完我也更清晰啦
欢迎查看我的博客