Rust编程进阶 - 如何基于生成器设计一套协程(Coroutine)的方案, 从而方便编写大规模高性能异步程序
Rust 设计这个生成器,主要目的在于,基于生成器设计一套协程(Coroutine)的方案, 从而方便编写大规模高性能异步程序。这个功能也是Rust 设计组2018年要解决的主要问题 之一,预计要到2018年年底才能正式稳定下来。到目前为止,依然只是一个实验性质的不 稳定功能。
所谓协程,指的是一种用户态的非抢占式的多任务机制。它也可以实现多任务并行。跟 线程相比,它的最大特点是它不是被内核调度的,而是由任务自己进行协作式的调度。协 程的实现方案一般可以分为stackful 以 及stackless 两种。Rust 的协程采用的是stackless coroutine 的设计思路。
在Rust 语言和标准库中,只引入了极少数的关键字、 trait 和类型。async 和 await 关键 字是目前许多语言都采用的主流方案,使用关键字而不是用宏来做API, 有助于社区的统一 性,避免不同的异步方案使用完全不一样的用户API 。引入关键字使用的是edition 方案,所
以不会造成代码不兼容问题。标准库中只有极少数必须的类型,这也是Rust 一贯的设计思 路。但凡是可以在第三方库中实现的, 一律在第三方库中实现,哪怕这个库本来就是官方核 心组维护的,这样做可以让这个库的版本升级更灵活,有助于标准库的稳定性。
Rust 的协程设计,核心是async 和 await两个关键字,以及Future 这 个trait:
pub trait Future {type Output;fn poll(self: PinMut < Self > , cx: &mut Context) - >Poll < Self: :Output > ;
}
Future 这个trait 代表的是异步执行。它里面最重要的一个方法是poll, 意思是,查看 当前Future 的状态,这个方法的返回类型如下:
pub enum Poll < T > {Ready(T),Pending,
}
对于一个实现了Future Trait的类型,每次调用这个 poll 方法,其实就是查看一下这个 对象当前的状态是什么,该状态可以为正在执行或者已经执行完毕。
Future 可以组合, 一个Future 可以由其他的一个或者多个Future=包装而成。跟我们已经 见过的迭代器Iterator 很像。比如,我们可以实现一个新的Future, 它的结果是多个Future=按顺序执行得到的。或者,实现一个Future, 它的结果是两个子Future 中先返回的那个。 Future 组合的方式可以非常灵活。
然后我们还需要一个调度器Executor, 标准库中有一个Executor 的 trait 。它的具体实现 可以由第三方库来实现。它应该有一个主事件循环,不断调用最外层每个收到了事件通知的 Future 的 poll 方法,外层的 Future 的 poll 方法被调用时,它就会调用内层Future 的 poll 方 法,不断嵌套。如果这个Future 处 于Pending 状态,那么这个Future 就应该设置好自己需要 监听的事件信息,然后马上返回,放弃占用CPU 。等到合适的事件发生时,调度器则应该再 次调用这个Future 的 poll 方法,驱动这个Future 从上次退出的那里继续往下执行。
大家可以看到,Future 跟 Generator 一样,具备同样的特性,也就是说可以在某个地方主 动中断执行,待下一次再进来的时候,刚好可以从上次退出的地方恢复执行。这就是为什么 Rust 的 Future 最终是基于Generator 实现的。在Rust 里面,Generator 是 Future 的基础设施。
关于这个Future trait, 另外一个需要注意的点是,它的self参数是PinMut类型,而Generator 的 resume 方法的self参数是&mut Self 类型。这个PinMut类型, 也是一个智能指针,它的目的主要就是保证它所指向的对象无法被move 。 而 &mut Self类型是无法保证这一点的。举个例子,大家还记得 std::mem::swap方法吗?
pub fn swap<T>(x:&mut T,y:&mut T)
对于任意两个T 类型的对象,如果我们拥有指向它们的&mut T 型指针,就可以把它们互换位置。这个操作就相当于把这两个对象都move 到了其他地方。如果这两个对象存在 自引用的现象,那么这个 swap 操作就可以造成它们内部出现野指针。而PinMut 类 型 就不存在这样的问题。PinMut还实现了DerefMut trait,所以它依然有权调用那些需 要 &mut Self 的成员方法。正因为PinMut 保证了指向的对象不可move, 所以这个 poll 方 法就可以不用unsafe 修饰了。
一般情况下,实现任务调度以及为通过各种异步操作实现Future trait并不是最终用户 关注的问题,这些应该都已经被网络开发框架完成,比如 tokio。大部分用户需要关注的是 如何利用这些框架完成业务逻辑。用户此时用得最多的是async和 await关键字。在最新的 nightly 版本中,只有async 关键字的实现已完成,await 关键字还存在争议,它目前依然是使 用宏来实现的。一个基本的使用示例如下所示:
async fn async_fn(x: u8) - >u8 {let msg = await ! (read_from_network());let result = await ! (calculate(msg, x));result {}
在这个示例中,假设 read_from_network() 以 及calculate() 函数都是异步 的。最外层的async_fn 函数当然也是异步的。当代码执行到await!(read_from_network()) 里面的时候,发现异步操作还没有完成,它会直接退出当前这个函数,把 CPU 让给其他任务执行。当这个数据从网络上传输完成了,调度器会再次调用这个函数,它 会从上次中断的地方恢复执行。所以用async/await的语法写代码,异步代码的逻辑在源码 组织上跟同步代码的逻辑差别并不大。这里面状态保存和恢复这些琐碎的事情,都由编译器 帮我们完成了。
下面给大家解释一下async 和 await 分别做了什么事情。async关键字可以修饰函数、闭包以及代码块。对于函数:
async fn f1(arg:u8)->u8{}
实际上等同于:
fn f1(arg:u8)->impl Future<Output =u8>{}
这两种写法实际上是一模一样的。凡是被async 修饰的函数,返回的都是一个实现了 Future trait的类型。由async 修饰的闭包也是一样的。async 代码块同样类似。它相当于创建 了一个语句块表达式,这个表达式的返回类型是impl Future 。async关键字不仅对函数签名 做了一个改变,而且对函数体也自动做了一个包装,被async关键字包起来的部分,会自动 产生一个Generator, 并把这个Generator 包装成一个满足Future 约束的结构体。在函数体中 用户需要返回的是Future::Output 类型。
对于await 这个宏,我们可以在标准库中看到它的实现:
macro_rules ! await { ($e: expr) = >{{let mut pinned = $e;let mut pinned = unsafe {$crate: :mem: :PinMut: :new_unchecked( & mut pinned)};loop {match $crate: :future: :poll_in_task_cx( & mut pinned) {$crate: :task: :Poll: :Pending = >yield,$crate: :task: :Poll: :Ready(x) = >break x,}}}}
从语法上来讲,await 一定只能在async 函数或代码块中出现,所以它实际上是被包在一 个Generator 里面的。await 这个宏的逻辑也很简单,await!(another_future)所做的 事情就是,先构造一个PinMut 指针指向 another_future, 然后调用 another_future的poll 方法。如果其处于Pending 状态则yield, 暂时退出这个Future。每当调度器恢复它的 执行时,它都会继续调用poll 方法,直到处于Ready 状态,这时候这个await 就算是执行完 毕了,继续执行后面的语句。
下面我们看一下,如果基于async/await 写程序,看起来会是什么样子:
async fn fetch_rust_lang(client: hyper: :Client) - >io: :Result < String > {let response = await ! (client.get("https://www.rust-lang.org")) ? ;if ! response.status().is_success() {return Err(io: :Error: :new(io: :ErrorKind: :Other, "request failed"))}let body = await ! (response.body().concat()) ? ;let string = String: :from_utf8(body) ? ;Ok(string)
}
可以看到,使用async/await 来写异步逻辑,一方面可以保证高效率,另一方面,代码 流程还是跟普通的同步逻辑类似,比较符合直觉。当然,我们也可以不用这个语法,通过 Future的各种adapter 的组合来完成同样的功能,这就跟上文中的对比一样,其与async/await 的区别,就与手写Iterator 和Generator 之间的区别一样。
