原文:https://stjepang.github.io/2020/01/31/build-your-own-executor.html
现在我们已经构建了block_on函数,是时候进一步将其转换为一个真正的执行器了。我们希望我们的遗执行器不只是一次运行一个future,而是同时运行多个future!
这篇博文的灵感来自于 juliex,一个最小的执行器,作者也是Rust中的async/await功能的开拓者之一。今天我们要从头开始写一个更现代、更清晰的juliex
版本。
我们的执行器的目标是只使用简单和完全安全的代码,但是性能可以与现有的最佳执行器匹敌。
我们将用作依赖的crate包括 crossbeam、 async-task、 once_cell、 futures 和 num_cpus。
接口
执行器只有一个函数,就是运行一个future:
fn spawn<F, R>(future: F) -> JoinHandle<R>where F: Future<Output = R> + Send + 'static, R: Send + 'static,{ todo!()}
返回的JoinHandle是一种实现了Future的类型,在任务完成后可以取得其输出。
注意这个spawn()函数和 std::thread::spawn()之间的相似之处——它们几乎是等价的,除了一个产生异步任务,另一个产生线程。
下面是一个简单的例子,生成一个任务并等待它的输出:
fn main() { futures::executor::block_on(async { let handle = spawn(async { 1 + 2 }); assert_eq!(handle.await, 3); });}
将输出传递给JoinHandle
既然 JoinHandle是一个实现 Future 的类型,那么让我们暂先简单地将它定义为一个固定到堆上的future的别名:
type JoinHandle<R> = Pin<Box<dyn Future<Output = R> + Send>>;
这个方法目前可行,但是不要担心,稍后我们会将它作为一个新的结构清晰地重写,并手动实现 Future。
产生的 future 的输出必须以某种方式发送到 JoinHandle。一种方法是创建一个 oneshot 通道,并在future完成时通过该通道发送输出。那么 JoinHandle 就是一个等待来自通道的消息的future:
use futures::channel::oneshot;
fn spawn<F, R>(future: F) -> JoinHandle<R>where F: Future<Output = R> + Send + 'static, R: Send + 'static,{ let (s, r) = oneshot::channel(); let future = async move { let _ = s.send(future.await); };
todo!()
Box::pin(async { r.await.unwrap() })}
下一步是在堆上分配future包装器,并将其推入某种全局任务队列,以便由执行程序处理。我们称这种分配的future为一项任务。
任务的剖析
任务(task)包括future和它的状态。我们需要跟踪状态,以了解任务是否计划运行、是否当前正在运行、是否已经完成等等。
下面是我们的Task类型的定义:
struct Task { state: AtomicUsize, future: Mutex<Pin<Box<dyn Future<Output = ()> + Send>>>,}
我们还没有确定状态到底是什么,但它将是某种可以从任何线程更新的 AtomicUsize。我们以后再说吧。
Future 的输出类型是()ーー这是因为 spawn ()函数将原始的 future 包装成一个将输出发送到 oneshot 通道,然后简单地返回()。
future被固定在堆上。这是因为只有pin的future才能被轮询(poll)。但是为什么它还被包装在Mutex中呢?
每个与任务相关联的 Waker 都会保存一个 Task 引用,这样它就可以通过将任务推入全局任务队列来唤醒任务。问题就在这里: 任务实例在线程之间共享,但是轮询future需要对它的可变访问。解决方