【译】理解Rust中的Futures (一)
原文标题:Understanding Futures In Rust -- Part 1
原文链接:https://www.viget.com/articles/understanding-futures-in-rust-part-1/
公众号: Rust 碎碎念
翻译 by: Praying
背景
Rust 中的 Futures 类似于 Javascript 中的promise[1],它们是对 Rust 中并发原语的强大抽象。这也是通往async/await[2]的基石,async/await 能够让用户像写同步代码一样来写异步代码。
Async/await 在 Rust 初期还没有准备好,但是这并不意味着你不应该在你的 Rust 项目中开始使用 futures。tokio[3] crate 稳定、易用且快速。请查看此文档[4]来了解使用 future 的入门知识。
Futures 已经在标准库当中了,但是在这个系列的博客中,我打算写一个简化版本来展示它是如何工作的、如何使用它以及避免一些常见的陷阱。
Tokio 的主分支正在使用 std::future,但是所有的文档都引用自 0.1 版本的 futures。不过,这些概念都是适用的。
尽管 futures 现在在 std 当中,但是缺失了很多常用的特性。这些特性当前在 future-preview[5] 中维护,并且我将会引用定义在其中的函数和 trait。事情进展得很快,那个 crate 里的很多东西最终都会进入标准库。
预备知识
了解一些 Rust 的知识或者在接下来的过程中愿意去学习 Rust(能够阅读Rust book[6]就更好了。)
一个现代的浏览器,比如 Chrome,FireFox,Safari,或者 Edge(我们将会使用 rust playground[7])
就这些!
目标
本文的目标是能够理解下面的代码,并且实现所需的类型和函数来使其能够编译。这段代码对于标准库的 futures 是有效的语法,并且说明链式 futures 是如何工作的。
// 这段代码目前还不能编译
fn main() {
let future1 = future::ok::<u32, u32>(1)
.map(|x| x + 3)
.map_err(|e| println!("Error: {:?}", e))
.and_then(|x| Ok(x - 3))
.then(|res| {
match res {
Ok(val) => Ok(val + 3),
err => err,
}
});
let joined_future = future::join(future1, future::err::<u32, u32>(2));
let val = block_on(joined_future);
assert_eq!(val, (Ok(4), Err(2)));
}
Future 到底是什么?
具体来讲,它是一系列异步计算所代表的值。Futures crate 的文档称其为“表示一个对象,该对象是另一个尚未准备好的值的代理(a concept for an object which is a proxy for another value that may not be ready yet)”。
Rust 中的 futures 允许你定义一个可以被异步运行的任务,比如一个网络调用或者计算。你可以在那个结果上链接函数,对其进行转换,处理错误,与其他的 futures 合并以及执行许多其他的计算。这些函数只有当 future 被传递给一个 executor,比如 tokio 的run函数,才会执行。事实上,如果你在离开作用域之前没有使用 future,什么事都不会发生。也因此,futures crate 声明 futures 是must_use的,并且如果你允许它们没有被使用就离开作用域,编译器会给出一个警告。
如果你熟悉 JavaScript 的 promises,有些东西可能会觉得奇怪。在 JavaScript 中,promises 是在事件循环中被执行,并且没有其他的可以运行它们的选择。executor函数是立即运行的。但是,从本质上来讲,promise 仍然只是简单地定义了一系列将来要执行的指令。在 Rust 中,executor 可以选择许多异步策略中的任意一个来运行。
构建我们的 Future
从高一点的层次来讲,我们需要一些代码片段来让 futures 工作;一个 runner,future trait 以及 poll 类型。
首先,一个 Runner
如果我们没有一种方式来执行我们的 future,它将不会做什么事情。因为,我们正在实现我们自己的 futures,所以我们也需要实现我们自己的 runner。在这个练习中,我们实际上不会做任何异步的事情,但是我们将会进行近似的异步调用。
Futures 基于 pull 而不是基于 push。这使得 futures 能够成为一个零抽象,但是这也意味着它们会被轮询一次,并且在当它们准备能够再次轮询的时候负责提醒 executor。它工作方式的具体细节对于理解 futures 是如何被创建和链接到一起并不重要,因此,我们的 executor 只是一个非常粗略的近似。它只能运行一个 future,并且它不能做任何有意义的异步。Tokio 文档有很多关于 futures 运行时模型的信息。
下面是一个看起来非常简单的实现:
use std::cell::RefCell;
thread_local!(static NOTIFY: RefCell<bool> = RefCell::new(true));
struct Context<'a> {
waker: &'a Waker,
}
impl<'a> Context<'a> {
fn from_waker(waker: &'a Waker) -> Self {
Context { waker }
}
fn waker(&self) -> &'a Waker {
&self.waker
}
}
struct Waker;
impl Waker {
fn wake(&self) {
NOTIFY.with(|f| *f.borrow_mut() = true)
}
}
fn run<F>(mut f: F) -> F::Output
where
F: Future,
{
NOTIFY.with(|n| loop {
if *n.borrow() {
*n.borrow_mut() = false;
let ctx = Context::from_waker(&Waker);
if let Poll::Ready(val) = f.poll(&ctx) {
return val;
}
}
})
}
run是一个泛型函数,其中 F 是一个 future,并且它返回一个定义在Future trait 中的Output类型的值,我们在后面会讲到它。
函数体的逻辑近似于一个真实的 runner 可能会做的事情,它会一直循环直到被提醒 future 准备好被再次轮询了。它会在 future 就绪时从函数返回。Context和Waker类型是对定义在future::task模块中的同名类型的模拟,可以在这里[8]看到。编译需要这里有它们的存在,但是这不再本文的讨论范围之内。具体它们是怎么实现的,你可以去自由探索。
Poll 是一个简单的泛型枚举,我们可以像下面这样定义它:
enum Poll<T> {
Ready(T),
Pending
}
我们的 Trait
Trait[9]是在 Rust 中定义共享行为的一种方式。它允许我们能够指定实现类型必须定义的类型和函数。它还可以实现默认的行为,这会在我们讲到组合器(combinator)的时候看到。
我们的 trait 实现看起来像下面这样(这和真实的 futures 实现是一致的):
trait Future {
type Output;
fn poll(&mut self, ctx: &Context) -> Poll<Self::Output>;
}
这个 trait 现在还很简单,只是声明了所需的类型——Output,以及唯一需要的方法的签名——poll,poll方法持有一个 context 对象的引用。这个对象持有一个对 waker 的引用,waker 被用于提醒运行时(runtime)future 准备好被再次轮询。
我们的实现
#[derive(Default)]
struct MyFuture {
count: u32,
}
impl Future for MyFuture {
type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
match self.count {
3 => Poll::Ready(3),
_ => {
self.count += 1;
ctx.waker().wake();
Poll::Pending
}
}
}
}
让我们一行一行地来看上面的代码:
#[derive(Default)]为这个类型自动创建一个::default()函数。数值类型(即这里的count)默认为 0。struct MyFuture { count: u32 }定义了一个带有一个计数器(count)的简单结构体。这让我们能够模拟异步行为。impl Future for MyFuture是我们对这个 trait 的实现。我们把 Output 设置为
i32类型,因此我们可以返回内部的计数。在我们的
poll实现中,我们基于内部的 count 字段决定要做什么、如果它匹配了 3
3=>,我们返回一个带有值为 3 的Poll::Ready响应。在其他情况下,我们增加计数器的值并且返回
Poll::Pending
加上一个简单的 main 函数,我们可以运行我们的 future 了!
fn main() {
let my_future = MyFuture::default();
println!("Output: {}", run(my_future));
}
自己运行一下![10]
最后一步
这就是它的工作原理,但是没有真正地向你展示出 futures 的强大。所以,让我们创建一个超级便利的 future,用它来链接到任意任意可以加 1 的类型来进行加 1 操作,例如,MyFuture。
struct AddOneFuture<T>(T);
impl<T> Future for AddOneFuture<T>
where
T: Future,
T::Output: std::ops::Add<i32, Output = i32>,
{
type Output = i32;
fn poll(&mut self, ctx: &Context) -> Poll<Self::Output> {
match self.0.poll(ctx) {
Poll::Ready(count) => Poll::Ready(count + 1),
Poll::Pending => Poll::Pending,
}
}
}
这段代码看起来复杂但实际上非常简单。我会再次一行一行地来回顾:
struct AddOneFuture<T>(T);这是一个泛型newtype[11]模式的示例。它让我们能够wrap其他的结构体并且添加我们自己的行为。impl<T> Future for AddOneFuture<T>是一个泛型 trait 实现。T: Future保证被 AddOneFuture wrap 的任意东西实现了 Future。T::Item: std::ops::Add<i32, Output=i32>确保了Poll::Ready(value)表示的值有对应的+操作。
剩下的部分就很容易看懂了。它使用self.0.poll轮询内部的 future,贯穿上下文,并且根据结果要么返回Poll::Pending或者返回内部 future 的计数加 1——Poll::Ready(count + 1)
我们可以只更新main函数以使用我们的新的 future。
fn main() {
let my_future = MyFuture::default();
println!("Output: {}", run(AddOneFuture(my_future)));
}
自己运行一下![12]
现在,我们能够看到我们是如何使用 futures 把异步行为链接到一起。只需要几个简单步骤,就可以建立为 futures 赋予强大能力的链式函数(combinators)。
概要
Future 是一种利用 Rust 零成本抽象概念来实现良好可读性、快速的异步代码的强大方式。
Futures 行为和 JavaScript 以及其他语言中的 promise 很像。
我们已经学到了很多关于构建通用类型和一部分将行为链接到一起的内容。
接下来
在 part 2[13],我们将讨论组合器(combinators)。组合器,在非技术性方面,能够让你使用函数(比如回调函数)来构建一个新类型。如果你已经用过 JavaScript 的 promises,这些将会很熟悉。

参考资料
promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[2]
async/await: https://areweasyncyet.rs/
[3]
tokio: https://tokio.rs/
[4]
此文档: https://tokio.rs/docs/futures/overview/
[5]
future-preview: https://docs.rs/futures-preview/0.3.0-alpha.17/futures/
[6]
Rust book: https://doc.rust-lang.org/stable/book/
[7]
rust playground: https://play.rust-lang.org/
[8]
这里: https://docs.rs/futures-preview/0.3.0-alpha.17/futures/task/index.html
[9]
Trait: https://doc.rust-lang.org/book/ch10-02-traits.html
[10]
自己运行一下!: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=254b419cb4a9229b67219400890c9e9b
[11]
newtype: https://github.com/rust-unofficial/patterns/blob/master/patterns/newtype.md
[12]
自己运行一下!: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=82df3f3ae9ab242d1d536bf9c851349d
[13]
part 2: https://www.viget.com/articles/understanding-futures-is-rust-part-2/
【译】理解Rust中的Futures (一)的更多相关文章
- 【译】理解Rust中的Futures(二)
原文标题:Understanding Futures in Rust -- Part 2 原文链接:https://www.viget.com/articles/understanding-futur ...
- 【译】理解Rust中的闭包
原文标题:Understanding Closures in Rust 原文链接:https://medium.com/swlh/understanding-closures-in-rust-21f2 ...
- 【译】理解Rust中的局部移动
原文标题:Understanding Partial Moves in Rust 原文链接:https://whileydave.com/2020/11/30/understanding-partia ...
- 【译】深入理解Rust中的生命周期
原文标题:Understanding Rust Lifetimes 原文链接:https://medium.com/nearprotocol/understanding-rust-lifetimes- ...
- 【译】Rust中的array、vector和slice
原文链接:https://hashrust.com/blog/arrays-vectors-and-slices-in-rust/ 原文标题:Arrays, vectors and slices in ...
- [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()
译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...
- 刷完欧拉计划中难度系数为5%的所有63道题,我学会了Rust中的哪些知识点?
我为什么学Rust? 2019年6月18日,Facebook发布了数字货币Libra的技术白皮书,我也第一时间体验了一下它的智能合约编程语言MOVE,发现这个MOVE是用Rust编写的,看来想准确理解 ...
- [译]线程生命周期-理解Java中的线程状态
线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...
- Rust初步(四):在rust中处理时间
这个看起来是一个很小的问题,我们如果是在.NET里面的话,很简单地可以直接使用System.DateTime.Now获取到当前时间,还可以进行各种不同的计算或者输出.但是这样一个问题,在rust里面, ...
随机推荐
- 冷门的HTML - tabindex 的作用
冷门的HTML - tabindex 的作用 HTML 的 tabindex 属性开发过程中一般不会使用到,最近开发中有个需求兼顾富交互,便总结了一下.本篇文章同时收录在我的[前端知识点]中,Gith ...
- .NET 开源工作流: Slickflow流程引擎高级开发(八) -- 审批网关(ApprovalOrSplit)模式的应用
前言:业务流程流转过程中,审批类型的节点是比较常见的,在审批操作中,常见的操作就是就是主管人员对待办事项进行同意或者拒绝.所以网关处理节点,就是需要对这两种审批结果进行预备处理,审批网关是在或分支(O ...
- 【进阶之路】Redis基础知识两篇就满足(一)
导言 大家好,我是南橘,一名练习时常两年半的java练习生,这是我在博客园的第一篇文章,当然,都是要从别处搬运过来的,不过以后新的文章也会在博客园同步发布,希望大家能多多支持^_^ 这篇文章的出现,首 ...
- 打包错误:Failed to execute goal org.scala-tools:maven-scala-plugin:2.15.2:compile (default) on project MusicProject: wrap: org.apache.commons.exec.ExecuteException:
错误:Failed to execute goal org.scala-tools:maven-scala-plugin:2.15.2:compile (default) on project Mus ...
- 云计算之路-出海记:建一个免费仓库 Amazon RDS for SQL Server
上周由于园子后院起火,不得不调兵回去救火,出海记暂时停更,这周继续更新,"出海记"记录的是我们在 AWS 上建设博客园海外站的历程. 在这一记中记录的是我们基于 AWS 免费套餐( ...
- 【惊喜】Github爆火的java面试神技+java核心面试技术已开发下载,大厂内都传疯了!
前言 今年,由于疫情的影响,很多互联网企业都在缩减招聘成本.作为程序员,原本这两年就面临竞争激烈.年龄危机的问题,而现在的求职局面又完全是企业在挑人的状态. 所以最好能在空闲的时候看看大厂相匹配的技术 ...
- 如何用OCR文字识别软件将PDF转换成Excel
最近老板老是让小编处理PDF文件,这OCR识别软件咱也不懂,也不敢问,只能一字一字的码在Excel上,但是这波操作效率不高,还没完成任务,老板又发了一堆PDF文件需要处理,怎么办呢? 跟朋友说了这事后 ...
- 加密PDF文件,提高文件安全性
PDF文件的一大优点是可以设置文件的安全性,不仅可以通过证书加密的形式加密文件,还可以通过pdfFactory来设置密码的形式加密文件. 我们可以通过两种方式开启"PDF加密"来为 ...
- Linux中redis服务开启
集群模式设置为no 就可以开启服务 cluster-enable no
- netty&websocket
1.先判断是不是http 消息,不是返回400,是则remove之前添加的http组件,动态添加websocket组件 添加WebSocket Encoder和WebSocket Decoder之后, ...