--- 摄于 2017 年 9 月 藏川线前段
上俩话,主要介绍异步的背景,包括提高资源利用率和硬件软件方方面面做的一些努力。基本上来说,有了上俩话的铺垫,有能力的同学就可以自己着手去实现一个自己的异步运行时了,当然,编程语言的不同导致实现的方式大不相同,但方轮子应该是没啥问题了。这一话,我们来说说 Rust 的异步方案。
众所周知,Rust 在 1.0 前紧急将内置的绿色线程实现给干掉了,在标准库里面提供了一层非常薄的 runtime。在 Rust 设计理念不改动的前提下,绿色线程重新加入 std 是基本不可能的事情了。
通过上一话,我们知道,异步实际上就是将无用的阻塞暂停时间利用起来,把暂停的任务调度走,将可以运行的任务放在线程中执行,无论怎么定义,这种类似于绿色线程的调度模式是必须要存在的。由于 Rust std 中已经去除了绿色线程的存在,又需要对此类任务进行社区统一,Rust 开发组转而对异步任务本身进行统一抽象,统一了异步任务的表达,社区的任何 runtime 实现都可以执行任意实现了异步抽象的任务。
任何异步任务,都可以有两种状态输出,暂停(pending)和完成(ready)。通过一种模式将这种输出方式表达出来,就是 Future trait:
enum Poll<T> {
Ready(T),
Pending
}
trait Future {
type Output;
fn poll(&mut self) -> Poll<Self::Output>;
}
这就是最初的异步任务表达,至于后续的 Pin 和 Context,我们后面再谈,这里可以暂时忽略。
至于为什么要选 Poll 模型,而不是其他的模型,可以从上一话中得到一定的解释,无论是就绪式通知还是完成式通知,用户必须去尝试读取和写入才能知道答案,poll 可以理解为一种尝试操作,得到不能执行后,就返回 Pending 状态,runtime 就将该 task(Future)移出执行队列,再执行下一个可以执行的任务。
我们已经将 task 定义为 poll base 的 Future 了,接下来就需要考虑怎么去执行它了。这里介绍一种通用的模型,就是 React + Executor 模型。所谓的 React 就是等待 OS 提供可执行信号,再简单一点,就是执行 epoll_wait
的线程,React 得到可执行的 fd 列表后,通知 Executor 将对应的 Future 拿出来依次调用 poll 函数:
这里涉及到如何通知可执行队列的问题,在早期,这个通知机制由 runtime 实现的时候自行解决,但由于通知机制实际上是 Future 标准的一部分,所以,后来将 Future trait 改成了:
trait Future {
type Output;
fn poll(&mut self,cx: &mut Context<'_>) -> Poll<Self::Output>;
}
本来通知机制只需要一个 Waker 就可以了,Rust 组为了保证兼容性,未来也许可能会在这里加上其他的东西,所以将 Waker 包装成了 Context。所谓的 Waker 机制,实际上就是通知机制,给予 Future 本身以唤醒自己的能力,或者说,这是一套通知约定,由 runtime 实现,并赋予每一个 Future task。
为什么需要显式将 waker 写入 Future trait?
首先,这是一套统一整个生态的通用机制。其次,除了依赖 OS 唤醒的任务以外,还有一些需要自我唤醒的 Future,它需要在 Pending 状态的时候,保存住传入的 Waker,当条件达到的时候,调用 Waker.wake()
,用来自我唤醒,典型的应用有 channel,async lock, async semaphore 等异步环境下的同步元语。之后可能会有单独一节讲讲最简单的 channel 是怎么实现的(不确定)。
众所周知,Rust 语言里最特别的东西就是 stack borrow check 和默认 Move 语义。它带来了一系列的好处,大量内存问题由于它的存在而无所遁形。但它也带来了一些问题,著名的问题就是自引用问题。
Future 的运行,会改变和记录自身的状态,如果存在自引用结构,当这个 Future 被 move 的时候,解引用它的时候,是一个 UB 行为。为了解决这个问题,引入了 Pin 这样的智能指针,保证内部数据在不使用 unsafe 的前提下,不会被 move。
于是 Future trait 被改成了:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;
}
具体 Pin 的用法和意义可以参考官方文档
这一话,就是 Rust Future 的核心东西了,后面应该会说说如何正确定义和手写一个 Future。相较于 goroutine 来说,Rust 的异步方案对用户的要求更高,需要关注的东西更多,带来的自由度也更高。goroutine 也有它的好处,简单且内置于语言之中,真正的大统一,但代价也是非凡的,从上到下的全部重写,与 c 生态的割裂,cgo 的拉跨等等。至于有栈/无栈协程的区别,私以为并不是特别重要,Future 的具体实现就可以看成是一个 task 的栈信息,只是相对于统一分配会更小一点,更紧致一点。
请登录后评论
评论区
加载更多