众所周知Rust中的异步async/.await代码在编译时会被转换为Future+Waker+Executor的事件状态机的执行;
下面我们通过标准库和futures库提供的功能,手写Future的实现,加深对async/.await编译后Future执行的理解;
源代码:
Rust中Future执行底层探秘 关于Future Trait底层逻辑,Rust圣经中讲的已经非常到位了:
但是其中的例子举的是一个Sleep定时器的例子,基本上只会执行一次 poll,并且没有内部的逻辑;
后面我会再写另外的一个例子,虽然也没什么意义,但是增加了一些逻辑;
Rust圣经中例子的实现 概述 首先我们先来实现 Rust 圣经中计时器的例子;
对于一个 Async 系统而言,主要分为这么几个部分:
执行器 Executor:本质上是一个 mpsc Channel 的 Receiver + Waker 的实现;
唤醒器 Waker:Future 通过调用 waker 中的 wake 函数通知 Executor 调用 poll 函数推动 Future 任务完成;
Future(不知道怎么翻译,应该是期货的意思):包装 Task 以及 Waker,由 Executor 推动惰性执行;
任务 Task:实际异步执行的任务,被包装在 Future 中,任务完成后通过 Future 返回结果;
在 Rust 圣经中,上面的内容是穿插着讲解的,大体上是自底向上从 Future 开始;
这里会从 Executor 调度开始,自顶向下执行;
但是阅读下文之前,也希望你先阅读:
对 Rust 中的 Future 以及异步有一定的了解;
执行器 Executor Executor概述 Rust 的 Future 是惰性的,只有调度器调用了在 Future Trait 中定义的 poll 方法,才会真正被执行;
其中一个推动它的方式就是在 async 函数中使用 .await 来调用另一个 async 函数;
但是那些最外层的 async 函数,谁来推动它们运行呢?答案就是:执行器 executor !
任何一个异步系统,最终都需要一个统一的 Executor 来统一管理这些 Future 任务;
例如,futures 库中提供的 block_on:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 use futures::executor::block_on;struct Song { author: String , name: String , } async fn learn_song () -> Song { Song { author: "曲婉婷" .to_string (), name: String ::from ("《我的歌声里》" ), } } async fn sing_song (song: Song) { println! ( "给大家献上一首{}的{} ~ 你存在我深深的脑海里~~" , song.author, song.name, ); } async fn dance () { println! ("唱到情深处,身体不由自主的动了起来~ ~" ); } async fn learn_and_sing () { let song = learn_song ().await ; sing_song (song).await ; } async fn async_main () { let f1 = learn_and_sing (); let f2 = dance (); futures::join!(f1, f2); } fn main () { block_on (async_main ()); }
或者 tokio 框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 async fn say_world () { println! ("world" ) } #[tokio::main] async fn main () { let op = say_world (); println! ("hello" ); op.await ; } #![feature(prelude_import)] #[prelude_import] use std::prelude::rust_2018::*;#[macro_use] extern crate std;async fn say_world () { { ::std::io::_print(::core::fmt::Arguments::new_v1 (&["world\n" ], &[])); } } #[allow(dead_code)] fn main () { let body = async { let op = say_world (); { ::std::io::_print(::core::fmt::Arguments::new_v1 (&["hello\n" ], &[])); }; op.await ; }; #[allow(clippy::expect_used, clippy::diverging_sub_expression)] { return tokio::runtime::Builder::new_multi_thread () .enable_all () .build () .expect ("Failed building the Runtime" ) .block_on (body); } } #[rustc_main] pub fn main () -> () { extern crate test; test::test_main_static (&[]) }
其中 tokio::runtime::Builder::new_multi_thread() 就初始化了一个 Executor,只不过这个 Executor 是整个异步系统都在用,因此叫做 runtime 更为合适!
Executor 会管理一批 Future (最外层的 async 函数),然后通过不停地 poll 推动它们直到完成;
执行的逻辑如下:
最开始,Executor 会先 poll 一次 Future ,后面就不会主动去 poll 了,而是等待 Future 通过调用 wake 函数来通知它可以继续,它才会继续去 poll(类似于 Event-Loop);
这种wake 通知然后 poll的方式会不断重复,直到 Future 完成;
构建Executor 在 Rust 圣经的例子中构建的 Executor 更像是一个类似于线程池的调度器,性能其实并不会特别高;
在实际生产环境下,tokio 实现的是类似于 Go 中 goroutine 调度器的方式,将大量的 Future 映射到为数不多的操作系统中,并且通过提供可插拔的多个类型的 runtime 供你使用;
你可理解为,Rust 中的 tokio 既可以通过类似于 Java 中线程池的方式使用,也可以通过类似于 Go 中协程调度器的方式使用;
至于原因,当然是因为 There is no silver bullet,具体场景使用具体的模式;例如:嵌入式场景和服务器场景相差甚远!
Executor实现 Executor 需要从一个消息通道( channel )中拉取事件,然后运行;
当一个任务准备好后(可以继续执行),会将自己放入 Executor 的消息 Channel 中,然后等待执行器 poll ;
下面是 Executor 的定义和实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 pub struct Executor { ready_queue: Receiver<Arc<Task>>, } impl Executor { pub (crate ) fn run (&self ) { while let Ok (task) = self .ready_queue.recv () { let mut future_slot = task.future.lock ().unwrap (); if let Some (mut future) = future_slot.take () { let waker = waker_ref (&task); let context = &mut Context::from_waker (&waker); if future.as_mut ().poll (context).is_pending () { *future_slot = Some (future); } } } } }
可以看到,Executor 的实现是一个包含 Task 的 Receiver 用于接收来自 Spawner 创建的各个 Task 消息;
在 Executor 的 run 方法中,通过 while let 接收通道中的消息;
随后通过 take 将 Task 的所有权取出到 future 变量中,针对 Task 创建一个 Waker,并传入 Context 中;
最后,调用 future 的 poll 函数,推动任务执行,并且如果执行 poll 后仍然是 Pending 状态,则再次将 future 放回 Executor 的队列中去;
从上面可以看到,从 Executor 的角度,只能通过 ready_queue 获取 Future 来调度执行;
而创建并包装 Future 的工作是交给 Spawner 做的,下面来看;
实现Spawner Spawner负责创建新的Future,然后将它发送到任务通道中;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #[derive(Clone)] pub struct Spawner { task_sender: SyncSender<Arc<Task>>, } impl Spawner { pub 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 ("Channel已关闭" ); } }
显然,Spawner 的内部是一个 SyncSender,负责将传入的 Future 进行包装,然后通过 Channel 传入 Executor 中调度;
这正是 spawn 方法的逻辑:
首先,将 future 转为 Pinned Box,随后使用 Arc 进行了封装,最后通过 send 将包装好的任务发给了 Executor 执行;
将 future 转为 Pinned Box 的原因见:
下面是 Executor 和 Spawner 的构造函数:
1 2 3 4 5 6 7 pub 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 }) }
抽象Task Future 本质上就是一个 Task 任务的封装,下面来看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 struct Task { future: Mutex<Option <BoxFuture<'static , ()>>>, task_sender: SyncSender<Arc<Task>>, } impl ArcWake for Task { fn wake_by_ref (arc_self: &Arc<Self >) { let cloned = arc_self.clone (); arc_self.task_sender.send (cloned).expect ("任务队列已满" ); } }
在 Task 中,包装了 future 和一个从 Spawner clone 来的 task_sender,有了这个 task_sender,我们就可以在 Waker 中将自己发送给 Executor 执行;
在我们为 Task 实现 ArcWake Trait 时就是这么做的;
唤醒器 Waker 在异步场景下,Waker 是一个很重要的组件,因为我们不能让 Executor 不断的去轮询所有的 Future 来查看任务是否完成,而是应当在 Future 执行完成后主动的向 Executor 报告!
并且通常情况下,对于 Future 来说,第一次被 poll 时无法完成任务是很正常的;因此它需要确保在未来一旦准备好时,可以通知 Executor 再次对其进行 poll,进而继续往下执行,该通知就是通过 Waker 类型完成的;
下面是标准库中 Future Trait 和 Context 的定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pub trait Future { type Output ; fn poll (self : Pin<&mut Self >, cx: &mut Context<'_ >) -> Poll<Self ::Output>; } pub struct Context <'a > { waker: &'a Waker, _marker: PhantomData<fn (&'a ()) -> &'a ()>, }
可以看到,poll 方法的参数 cx 中包含了 waker,这正是上面我们在实现 Executor 中放入的;
当 Future 任务完成后,会调用 waker 的 wake 方法,注意到在前面我们为 Task 实现了 ArcWake Trait;
因此调用 waker 的 wake 方法后,会执行 Task 对应的方法:
1 2 3 4 5 fn wake_by_ref (arc_self: &Arc<Self >) { let cloned = arc_self.clone (); arc_self.task_sender.send (cloned).expect ("任务队列已满" ); }
最后再次将任务发送给了 Executor,由 Executor 调用 poll 推动 Future 修改状态!
需要注意的是:waker 调用 wake 后会消耗掉自己:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 pub fn wake (self ) { let wake = self .waker.vtable.wake; let data = self .waker.data; crate::mem::forget (self ); unsafe { (wake)(data) }; }
看到函数签名 wake(self)!
具体任务封装 Future 实际上,编写 Future 的任务是由编译器来做的:我们编写的每个 async 块在编译结束后都返回一个 Future 对象;
当然,这里为了学习,我们是手动创建的 Future:
1 2 3 4 5 6 7 8 9 10 11 12 pub struct TimerFuture { shared_state: Arc<Mutex<SharedState>>, } struct SharedState { completed: bool , waker: Option <Waker>, }
TimerFuture 的结构非常简单,仅仅维护了一个 completed 状态和一个waker:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 impl Future for TimerFuture { type Output = (); fn poll (self : Pin<&mut Self >, cx: &mut Context<'_ >) -> Poll<Self ::Output> { let mut shared_state = self .shared_state.lock ().unwrap (); if shared_state.completed { Poll::Ready (()) } else { shared_state.waker = Some (cx.waker ().clone ()); Poll::Pending } } }
可以看到,poll 中判断了是否是已完成:
如果已完成,标注当前 Future 已完成;
否则标记 Pending,并放回 waker(这是因为前面说到的waker 调用 wake 后会消耗掉自己: )
poll方法中只做具体 Future 状态的变化;
这个例子不好的一点在于,我们的 Future 中只有单个任务, 因此只有 Ready、Pending 状态;
如果存在多个顺序的协程,则会存在 Pending1、Pending2、…等等,多个状态切换;
还有一个不好的地方在于,他的任务只是简单的Sleep,甚至看不出来异步函数的逻辑究竟是在哪执行的!
既然 poll 方法只是推动 Future 的状态,那具体的 async 函数的逻辑在哪里执行呢?
答案是在 Future 本身被定义:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 impl TimerFuture { pub fn new (duration: Duration) -> Self { let shared_state = Arc::new (Mutex::new (SharedState { completed: false , waker: None , })); let thread_shared_state = shared_state.clone (); thread::spawn (move || { thread::sleep (duration); println! ("sleep task completed!" ); let mut shared_state = thread_shared_state.lock ().unwrap (); shared_state.completed = true ; if let Some (waker) = shared_state.waker.take () { waker.wake () } }); TimerFuture { shared_state } } }
上面是 Future 的构造函数,可以看到首先初始化了 shared_state,随后在一个新的线程中执行了任务(暂且把 Sleep 看作是一个高耗时的IO任务);
前面说到了,这里任务的提交是非常简单的,就是创建一个新的线程;
实际上,任务的提交后的调度是非常复杂的,很可能需要任务窃取、任务切换等一系列处理;
下面是 tokio 中 spawn 的实现,比我们这里复杂太多了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 pub (crate ) fn spawn <F>(&self , future: F, id: Id) -> JoinHandle<F::Output>where F: Future + Send + 'static , F::Output: Send + 'static , { match self { Handle::CurrentThread (h) => current_thread::Handle::spawn (h, future, id), #[cfg(all(feature = "rt-multi-thread" , not(tokio_wasi)))] Handle::MultiThread (h) => multi_thread::Handle::spawn (h, future, id), } } impl Handle { pub (crate ) fn spawn <F>(me: &Arc<Self >, future: F, id: task::Id) -> JoinHandle<F::Output> where F: crate::future::Future + Send + 'static , F::Output: Send + 'static , { Self ::bind_new_task (me, future, id) } pub (crate ) fn shutdown (&self ) { self .close (); } pub (super ) fn bind_new_task <T>(me: &Arc<Self >, future: T, id: task::Id) -> JoinHandle<T::Output> where T: Future + Send + 'static , T::Output: Send + 'static , { let (handle, notified) = me.shared.owned.bind (future, me.clone (), id); if let Some (notified) = notified { me.schedule_task (notified, false ); } handle } } impl Handle { pub (super ) fn schedule_task (&self , task: Notified, is_yield: bool ) { CURRENT.with (|maybe_cx| { if let Some (cx) = maybe_cx { if self .ptr_eq (&cx.worker.handle) { if let Some (core) = cx.core.borrow_mut ().as_mut () { self .schedule_local (core, task, is_yield); return ; } } } self .shared.inject.push (task); self .shared.scheduler_metrics.inc_remote_schedule_count (); self .notify_parked (); }) } } ...
使用Future 让我们把视线收回来,既然手动写完了 Future 让我们来测试一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 fn timer_demo () { let (executor, spawner) = new_executor_and_spawner (); spawner.spawn ( TimerFuture::new (Duration::new (2 , 0 )), ); drop (spawner); executor.run (); } fn main () { println! ("\ntimer_demo:" ); timer_demo () }
首先创建了 executor 和 spawner,这和 tokio 中的初始化环境类似;
随后,通过 spawner.spawn 传入了一个 Future 任务:回忆我们之前的代码,这里会直接向 Channel 中发送一个消息,但是还没有调用 executor.run,因此消息不会被消费!
这也是 Rust 中异步为是惰性的原因!
随后,我们释放 spawner,因为所有的消息都已经发送完毕了;
最后,调用 executor.run(),开始消费 spawner 发送的消息!
一个添加字符串的例子 前面的例子只是 Sleep,并没有什么实际意义,下面我们来写一个稍微有那么点意义的 Future;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 pub struct StringProcessFuture { task: Arc<Mutex<StringProcessTask>>, } pub struct StringProcessTask { s: String , processor: fn (&mut String ), complete_checker: fn (&str ) -> bool , waker: Option <Waker>, } impl StringProcessTask { pub fn check_task (&self ) -> bool { (self .complete_checker)(&self .s) } pub fn process (&mut self ) { while !self .check_task () { (self .processor)(&mut self .s); println! ("cur len: {}" , self .s.len ()); thread::sleep (Duration::new (1 , 0 )); } } } impl Future for StringProcessFuture { type Output = String ; fn poll (self : Pin<&mut Self >, cx: &mut Context<'_ >) -> Poll<Self ::Output> { let mut task = self .task.lock ().unwrap (); if task.check_task () { Poll::Ready (task.s.clone ()) } else { task.waker = Some (cx.waker ().clone ()); Poll::Pending } } } impl StringProcessFuture { pub fn new (processor: fn (&mut String ), complete_checker: fn (&str ) -> bool ) -> Self { let task = Arc::new (Mutex::new (StringProcessTask { s: String ::new (), processor, complete_checker, waker: None , })); let shared_task = task.clone (); thread::spawn (move || { let mut shared_state = shared_task.lock ().unwrap (); shared_state.process (); if let Some (waker) = shared_state.waker.take () { waker.wake () } }); StringProcessFuture { task } } }
StringProcessFuture 要求传入:
构建一个字符串的方法:processor: fn(&mut String);
以及一个校验字符串已经满足的条件:complete_checker: fn(&str) -> bool;
在实际的任务中,我们依然使用 Sleep 模拟了 IO 操作,但是和之前的例子不同,我们将 Future 操作提取到了 process 方法中,使得逻辑更加清晰:
1 2 3 4 5 6 7 pub fn process (&mut self ) { while !self .check_task () { (self .processor)(&mut self .s); println! ("cur len: {}" , self .s.len ()); thread::sleep (Duration::new (1 , 0 )); } }
异步处理的 Task:
1 2 3 4 5 6 7 8 9 thread::spawn (move || { let mut shared_state = shared_task.lock ().unwrap (); shared_state.process (); if let Some (waker) = shared_state.waker.take () { waker.wake () } });
测试一下我们的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 fn string_process_demo () { let (executor, spawner) = new_executor_and_spawner (); spawner.spawn (async { println! ("before StringProcessFuture!" ); let res = StringProcessFuture::new ( |s: &mut String | { s.push_str (" hello " ); }, |s: &str | -> bool { s.len () > 10 }, ) .await ; println! ("res: {}" , res); }); spawner.spawn (async { println! ("before StringProcessFuture!" ); let res = StringProcessFuture::new ( |s: &mut String | { s.push_str (" world " ); }, |s: &str | -> bool { s.len () > 20 }, ) .await ; println! ("res: {}" , res); }); drop (spawner); executor.run (); } fn main () { println! ("\nstring_process_demo:" ); string_process_demo () }
执行后输出如下:
1 2 3 4 5 6 7 8 9 10 string_process_demo: before StringProcessFuture! before StringProcessFuture! cur len: 7 cur len: 7 cur len: 14 cur len: 14 cur len: 21 res: hello hello res: world world world
可以看到,两个 Future 是并行被调度的(因为我们开了两个线程);
附录 源代码:
文章参考: