【译】理解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里面, ...
随机推荐
- php 序列化和反序列化的作用及使用
1.序列化是什么意思呢? 序列化就是把本来不能直接存储的数据转换成可存储的数据,并且不会丢掉数据格式 serialize(); 2.反序列化是什么意思呢? 其实就是字面的意思,把序列化的数据,转换成我 ...
- Hadoop大数据平台搭建之前期配置(2)
环境:CentOS 7.4 (1708 DVD) 工具:VMware.MobaXterm 一. 克隆大数据集群 1. 选中已经进行了基本配置的虚拟机,进行克隆. 2. 此处改为"创建完整克 ...
- 攻克弹唱第七课(如何弹奏neon)
在本期文章中,笔者将通过Guitar Pro 7来跟大家研究一下neon的曲谱,顺便复习一下之前文章中说过的和弦技巧. 在<如何在指板上寻找特殊和弦(二)>这一期课程中,我们分析过如何使用 ...
- leetcode152. 乘积最大子序列
给定一个整数数组 nums ,找出一个序列中乘积最大的连续子序列(该序列至少包含一个数).示例 1:输入: [2,3,-2,4]输出: 6解释: 子数组 [2,3] 有最大乘积 6.示例 2:输入: ...
- iPhone/iOS开启个人热点的相关位置调整小结
冬至已到,圣诞将近,最近公司项目实在太多,三四个项目反复的切换真的让人焦头烂额,趁今天有点空,把维护的三个项目顺利送出,刚好可以缕缕思路,记录一下最近遇到的问题.说不着急那是假的,客户一天天的催的确实 ...
- kubelet CPU 使用率过高问题排查
kubelet CPU 使用率过高问题排查 问题背景 客户的k8s集群环境,发现所有的worker节点的kubelet进程的CPU使用率长时间占用过高,通过pidstat可以看到CPU使用率高达100 ...
- Error response from daemon: driver failed programming external connectivity on endpoint mysql3308 (
Docker启动容器报错. 1. 错误描述 [root@localhost nginx]# docker start mysql3308 Error response from daemon: dri ...
- HOOK API函数跳转详解
原文链接:https://blog.csdn.net/cosmoslife/article/details/7670951 结合课件逆向11分析
- mySQL初学者需要掌握的【数据库与表的基本操作】
本内容会持续更新的哦! 注:"字段"="列","记录''="行" 文章目录 一:数据库的基本操作 二.数据表的基本操作 1.创建与 ...
- `prometheus-net.DotNetRuntime` 获取 CLR 运行指标原理解析
prometheus-net.DotNetRuntime 介绍 Intro 前面集成 Prometheus 的文章中简单提到过,prometheus-net.DotNetRuntime 可以获取到一些 ...