2.1.Futures
Futures
fearless concurrency 是 Rust 值得大书特书的一点。它提出了一个想法,授予你并行处理的权限而不舍弃安全性。而且,Rust 作为底层程序设计语言,无需担忧它对并发的处理,不用自己去选择特定的并发实现策略。 这也意味着,如果我们想不同策略的用户可以共享代码,我们必须对策略抽象,以便将来可以做选择。
Futures 对计算抽象。Futures 描述 “what”,并使其独立于 “where” 和 “when” 。因此,Futures 旨在将代码拆解为小的可组合的操作,然后由自己的系统的一部分执行这些操作。让人们了解计算事务的本质,找到可以抽象之处。
Send 和 Sync
幸运的是,支持并发的 Rust 已经有了两个广为人知的有效的概念,它们对并发部分的共享程序进行了抽象:Send
和 Sync
。值得注意的是,Send
和 Sync
的特征抽象自并发处理策略,它们结构规整,且不指定实现。
简短概述:
Send
将计算中的传递数据抽象到另一个并发计算(我们称其为接收方),而对于发送方,将无法再次访问它。在许多程序设计语言中,通常都实施此策略,但是缺少语言层面的支持,而寄希望于你自己执行“丢掉访问”的操作。bugs 的常规来源:发送方保留发送内容的句柄,甚至在发送后也可以操作它们。 Rust 通过使这种行为摆到明面上来减轻此问题。类型可以是Send
,也可以不是(通过适当的特征实现标记),允许或不允许发送它们,并且借助所有权和借用规则阻止后续的访问。
Sync
是指在程序的两个并发部分之间共享数据。这是另一种常见的模式:由于向内存位置写入或在另一方正在写入时进行读取本来就不安全,因此需要通过同步来协调。^1 协调双方有许多常见的方式来达成共识,即不同时使用位于内存中的同一部分,例如互斥锁和自旋锁。同样,Rust 提供了(安全!)无需担心的选择项。Rust 可以让人自由地表达哪些需要同步,而无需对具体的实现连篇累牍。
请留心,我们是如何避免使用任何类似 *”thread””* 的词,而选择了”computation” 。Send
和 Sync
的全部功能是,减轻你需要熟悉抽象出的共享代码中共享了 what 的负担。 在实现时,你只需要知道哪种共享方法适用于当前类型。 这种实现方式保证了推断的局部性,该类型用户不受以后使用其他任何实现的影响。
Send
和 Sync
可以以有趣的方式组合,但这超出了本文的范围。 你可以在 Rust Book 找到示例。
总结: Rust 使我们能够安全地抽象出并发程序的重要属性,以及其数据的共享方式。它以一种非常轻量级的办法达成目标;语言本身标识的是 Send
和 Sync
,并在可能的条件下,通过派生这两个标记帮助我们达成目的。剩下的就是库要处理的问题了。
简单的计算视图
虽然计算是一个可以写一整本书的主题,但于我们而言,非常简单的视图足以:一系列可组合的操作,这些操作可以根据决策进行分支,不间断地运行,产生一个结果或产生一个错误
延迟计算
如上所述, Send
和 Sync
与数据有关。但是程序又不仅只与数据有关,还跟数据的计算相关。 这就是 Futures
的目的。我们将在下一章研究它如何工作。让我们换个通俗易懂的方式表述 Futures 。Futures 的计划是从:
- 执行 X
- 如果 X 执行成功, 执行 Y
转变成:
- 开始执行 X
- 一旦 X 执行成功, 则开始执行 Y
(译者注:原文为 Start doing X 和 start doing Y,start doing 应是指 Futures 对 X 和 Y 进行异步的初始化加载的准备,当 X 标记完成时,无需对 Y 做初始化加载的准备工作,可立即执行 Y 的计算操作。初始化加载是指准备好计算要用的数据。)
还记得介绍中有关 “延迟计算” 的话题吗?(译者注:指 1.2.std::future 与 futures-rs |《async-std 中文文档》| Rust 技术论坛)这就是所有延迟计算相关的内容了。无需告诉计算机要执行什么操作,和当下的意图,而是告诉它要做哪些初始化加载,以及对即将发生的可能性事件做何反应
面向开始
让我们看一个简单的函数,特别是返回值:
# use std::{fs::File, io, io::prelude::*};
#
fn read_file(path: &str) -> io::Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
你可以随时调用它,因此可以完全控制何时调用它。但这就是问题所在:调用它的那一刻,你就将控制权转移到被调用的函数,直到它最终返回一个值。
请注意,此返回值是关于过去的。过去有一个缺点:已经做出了所有决定。它有一个优点:结果可见。我们可以解包程序过去的计算结果,然后决定如何处理它。
但是我们想做的是,对计算进行抽象,然后让其他人选择如何运行。本质上讲,这与始终可以观察先期计算出的结果的常见思维相悖。因此,需找到一种无需运行,只对计算进行描述的类型(译者注:函数式编程思维,即该类型是计算函数的映射,只描述,不执行)。请再一次阅读这个函数:
# use std::{fs::File, io, io::prelude::*};
#
fn read_file(path: &str) -> io::Result<String> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
从时间顺序论,我们只能在调用函数之前或返回函数之后执行操作。 这显然不可取,我们需要的是,可以在程序正运行的时候执行其他操作。当运行并发代码时,也意味着,这个操作在第一次运行时,就有了启动并行任务的能力(因为放弃了控制权)。
在这会儿,不得不提到线程。但是线程是一个非常特别的并发基元,而我们要寻找的是一种对程序操作的抽象。
我们正在寻找的东西描绘的是,正在进行的工作要指向一个未来的结果。每当我们讲述 Rust 的某个事物时,几乎都是在提及某种特性(trait)。 让我们从 Future
trait 的不完整的定义开始:
# use std::{pin::Pin, task::{Context, Poll}};
#
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
仔细阅读,观察到的有以下内容:
- 通用的
Output
. - 提供
poll
函数,函数功能是允许我们查看当前计算的状态 - (暂时忽略
Pin
和Context
,先不对它们作深入理解)
每次调用 poll()
都会导致以下两种情况的其中一种:
- 计算已经完成,
poll
会返回Poll::Ready
- 计算尚未完成执行,
poll
会返回Poll::Pending
上述机制使我们可以从外部检查 Future
是否仍有未完成的工作,或最终是否完成并给出返回值。最简单(但不是最有效)的方法是不断循环执行 poll 函数。 这种办法当然是有优化的可能,而这正是一个好的运行时要做的。
请注意,在第1种情况发生后再次调用 poll
可能会导致混乱。更多详情,请查看 futures-docs 。
Async
尽管 Future
trait 在 Rust 中已经存在不短的时间,但构建和描述它们并不方便。为此,Rust 有了特殊的语法:async
。请看一个使用 async-std
实现 Future
的例子:
# extern crate async_std;
# use async_std::{fs::File, io, io::prelude::*};
#
async fn read_file(path: &str) -> io::Result<String> {
let mut file = File::open(path).await?;
let mut contents = String::new();
file.read_to_string(&mut contents).await?;
Ok(contents)
}
当同时执行两个或多个函数时,我们的运行时系统会处理当前正在进行的所有其他事件,以此来填充等待时间。
结论
从值的角度出发,我们寻找到了一些东西,它们表示了寻找的方向是为了得到以后可用的值。在此基础上,我们讨论了轮询的概念。
Future 是一种不代表任何值的数据类型,但在将来的某个时间点它有能力产生一个值。针对不同的用例,实现的方式千差万别,但对外暴露的接口会非常简洁。
接下来,我们将向您介绍 Tasks
,实际上,Futures 的运行借助了 Tasks
。
原文链接:book.async.rs/
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。