Rust 有一个新兴的异步系统。如果你的应用程序 IO 很重,你应该简单地“使用异步”,一切都会高效地工作。
您可以使用async fn,.await,让它在后台处理,而 CPU 会做一些有用的事情。
然后你学习添加 Tokio让它做任何事情,事情可能看起来很神奇。
幸运的是,计算机还不能神奇地工作,所以我们可以尝试简化事情并获得更好的理解。
Rust 中的异步函数只是常规函数的语法糖,但它不是直接返回值,而是为您提供一个实现Future trait的复杂状态机。
// 下面语法糖 |
编译器然后会生成适当的类型和实现。这很有帮助,但它仍然过于复杂。
我将重点介绍我认为最重要的两个组件,即 Futuretrait和executor,最后展示可用于运行异步代码的最小执行器实现。
让我们先从trait 开始。
Future trait
这个Future特性虽然是异步 Rust 工作原理的基础,但却是一个相当简单的特性。很简单,这里是完整的:
pub trait Future { |
Output关联类型表示Future最终会返回什么类型,poll方法将检查异步进程是否完成,如果完成了,将返回Poll::Ready(Self::Output),如果没有完成,则返回Poll::Pending。那个Pin<&mut Self>的存在有复杂的原因,在本文中我们将基本忽略。
最重要的是,一个Future本身什么都不做。它应该迅速返回一个Poll::Pending或Poll::Ready,这样程序就可以继续检查其他的Future,或者回去睡觉。Future的实际工作应该发生在其他地方。
关于poll方法的唯一其他有趣的事情是它需要一个Context方法。在写这篇文章的时候,上下文的一个目的是:为你提供(参考)一个Waker,以后可以唤醒Executor。我们接下来会讨论这个问题。
因此,异步函数被转化为植入式的Future类型,但有些东西最后必须是异步的。
为了说明这一点,让我们看看一个简单的future,它不返回任何东西,但会产生一个线程,并在它产生的线程完成后完成。
use std::sync::Arc; |
这是一大模板,但基本原理相对简单:
- 检查我们是否已经完成了future。如果没有,检查我们是否已经开始了异步计算
- 如果没有,在后台生成一个最终将完成计算的线程
- 在该线程中,将自己标记为已完成
- 并唤醒
- 然后返回一个挂起的状态
- 最后,如果我们最终完成了,则返回Ready状态。
Executor
异步拼图的另一块是执行器Executor。执行器主要需要轮询期货,在无事可做时进入睡眠状态,并提供一个合适的唤醒器来唤醒。许多实现,如tokio,提供了大量的附加功能,但从根本上说,它需要做的只是完成以下步骤:
当用户向执行器提供一个Future时,执行器将开始轮询,直到它最终返回一个值。这个轮询不是一个繁忙的循环;相反,执行器将等待,直到它收到一个唤醒信号。
这个信号可以来自任何线程,甚至是一个简单的C风格的信号处理程序,并且通常是特定于支撑操作的异步过程。
或者,以流程图的形式:
对于执行器的实施,信号来自哪里并不重要。只要它引起了唤醒,就应该再次轮询future。有了这些知识,我们可以构建一个基本的执行器。只是我们不必这样做;在标准库的文档中,你可以找到一个故意的、有点小毛病的执行器实现2,它就是这样实现的。
我可以写一个正确的执行器样本,就像我写一个future的例子一样,但碰巧我已经写了,并把它作为一个crate发布了。现在让我们来看看这个。
Beul
写这篇文章的主要原因是Beul的 1.0.0 版本。Beul 是一个基于上述想法的安全、简约的期货执行者。它非常简约,只有 84 行(注释)代码,其整个公共 API 如下所示:
pub fn execute<T>(f: impl Future<Output = T>) -> T |
除了功能齐全的异步框架之外,人们会用它来做什么?主要原因是编写与执行程序无关的异步代码的测试,以及在大部分同步代码中使用异步库。
可以嵌套调用beul::execute,您可以在异步代码中调用一些同步代码,而异步代码又使用 Beul 调用异步代码。在许多情况下,不这样做对性能更好,并确保.await尽可能进行任何异步函数调用。
其他:
- Pollster提供了与这个 crate 大致相同的实现,只是使用了一个小的不安全代码,可以从 Rust 1.68 中删除。它使用扩展特性来提供其阻塞功能。main()它最近还获得了一组(可选的)过程宏,允许您以可以正常执行的方式注释异步或测试。Beul 稍微更小一些,并使用动态调度来减少代码大小。
- futures-executor提供了几个执行器和实用程序来简化 futures 的工作。它的block_on执行器类似于 Beul 的 API,尽管对它的调用可以有意不嵌套。这也是一个相当大的板条箱。
- extreme具有与 Beul 相同的 API,但早于该Wake特征,因此必须使用不安全的 Rust 来实现其执行程序。extreme还根据 GNU 公共许可证获得许可,这可能使其不适用于许多应用程序。版本控制方案虽然很好,但有些特殊。
- Yet Another Async Runtime(或yaar)和safina-executor都提供了更多的通用执行器框架,而不仅仅是执行期货的东西。如果您需要更多,它们可能是有意义的,但对于简单的未来执行,Beul 应该足够了。
综上所述
异步 Rust 由重复轮询 futures 直到完成的Executor执行者组成。
当 future 仍然悬而未决时,executor 会在再次轮询 future 之前去做其他事情,通常是睡觉。重复直到完成。
有了这个,我希望我已经提供了一个关于引擎盖下发生的事情的直觉。