原文标题: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 就绪时从函数返回。ContextWaker类型是对定义在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,以及唯一需要的方法的签名——pollpoll方法持有一个 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 字段决定要做什么、

  • 如果它匹配了 33=>,我们返回一个带有值为 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,这些将会很熟悉。

参考资料

[1]

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 (一)的更多相关文章

  1. 【译】理解Rust中的Futures(二)

    原文标题:Understanding Futures in Rust -- Part 2 原文链接:https://www.viget.com/articles/understanding-futur ...

  2. 【译】理解Rust中的闭包

    原文标题:Understanding Closures in Rust 原文链接:https://medium.com/swlh/understanding-closures-in-rust-21f2 ...

  3. 【译】理解Rust中的局部移动

    原文标题:Understanding Partial Moves in Rust 原文链接:https://whileydave.com/2020/11/30/understanding-partia ...

  4. 【译】深入理解Rust中的生命周期

    原文标题:Understanding Rust Lifetimes 原文链接:https://medium.com/nearprotocol/understanding-rust-lifetimes- ...

  5. 【译】Rust中的array、vector和slice

    原文链接:https://hashrust.com/blog/arrays-vectors-and-slices-in-rust/ 原文标题:Arrays, vectors and slices in ...

  6. [NodeJs系列][译]理解NodeJs中的Event Loop、Timers以及process.nextTick()

    译者注: 为什么要翻译?其实在翻译这篇文章前,笔者有Google了一下中文翻译,看的不是很明白,所以才有自己翻译的打算,当然能力有限,文中或有错漏,欢迎指正. 文末会有几个小问题,大家不妨一起思考一下 ...

  7. 刷完欧拉计划中难度系数为5%的所有63道题,我学会了Rust中的哪些知识点?

    我为什么学Rust? 2019年6月18日,Facebook发布了数字货币Libra的技术白皮书,我也第一时间体验了一下它的智能合约编程语言MOVE,发现这个MOVE是用Rust编写的,看来想准确理解 ...

  8. [译]线程生命周期-理解Java中的线程状态

    线程生命周期-理解Java中的线程状态 在多线程编程环境下,理解线程生命周期和线程状态非常重要. 在上一篇教程中,我们已经学习了如何创建java线程:实现Runnable接口或者成为Thread的子类 ...

  9. Rust初步(四):在rust中处理时间

    这个看起来是一个很小的问题,我们如果是在.NET里面的话,很简单地可以直接使用System.DateTime.Now获取到当前时间,还可以进行各种不同的计算或者输出.但是这样一个问题,在rust里面, ...

随机推荐

  1. 通过Tomcat Manager拿shell

    一.通过弱口令登录Tomcat后台 二.制作木马.war 1安装JDK 2.写一个jsp小马(我的小马是6.jsp) 3.cmd进小马的目录,然后运行 jar cvf shell.war  6.jsp ...

  2. java面试官最爱问的垃圾回收机制,这位阿里P7大佬分析的属实到位

    前言 JVM 内存模型一共包括三个部分: 堆 ( Java代码可及的 Java堆 和 JVM自身使用的方法区). 栈 ( 服务Java方法的虚拟机栈 和 服务Native方法的本地方法栈 ) 保证程序 ...

  3. 【老孟Flutter】自定义文本步进组件

    交流 老孟Flutter博客(330个控件用法+实战入门系列文章):http://laomengit.com 欢迎加入Flutter交流群(微信:laomengit).关注公众号[老孟Flutter] ...

  4. 在FL Studio中如何更好地为人声加上混响(进阶教程)

    为人声加上混响是我们在处理人声过程中必不可少的一步.然而,除了直接在人声混音轨道加上混响插件进行调节以外,这里还有更为细节的做法可以达到更好的效果. 步骤一:使用均衡器 在为人声加上混响之前,我们应该 ...

  5. 统一软件开发过程(RUP)的概念和方法

    统一软件开发过程(Rational Unified Process,RUP)是一种面向对象且基于网络的程序开发方法论. 根据Rational(Rational Rose和统一建模语言的开发者)的说法, ...

  6. CLH lock queue的原理解释及Java实现

    目录 背景 原理解释 Java代码实现 定义QNode 定义Lock接口 定义CLHLock 使用场景 运行代码 代码输出 代码解释 CLHLock的加锁.释放锁过程 第一个使用CLHLock的线程自 ...

  7. Matlab 画图1

    plot函数 plot最简单的是plot(x,y),其中,x,y是一组数据 如果要画出\(y=x^2\)的图像 在Command Window中输入 x =[1 2 3]; y =[4 5 6]; p ...

  8. eclipse 老坑巨滑之内存溢出OOM

    绪:今天接手一个古老项目,tomcat6+jdk6.被   java.lang.OutOfMemoryError: PermGen space  啪啪打脸, 网上确实有很多解决方法,主要有三种类型:一 ...

  9. django项目初始化

    1.为了方便管理app,我们添加专门的apps文件夹来存放所有的app.结构如下 1.1设置完apps文件夹以后我们需要对配置文件做相应的更改 1.1.1.在seetings.py里添加django文 ...

  10. 第11.20节 Python 中正则表达式的扩展功能:后视断言、后视取反

    一. 引言 在<第11.19节 Python 中正则表达式的扩展功能:前视断言和前视取反>中老猿介绍了前视断言和前视取反,与二者对应的还有后视断言和后视取反. 二. (?<=-)后视 ...