[async_std]--2.1--Futures
[屁话说了那么多,终于到肉了]。
A notable point about Rust is fearless concurrency. That is the notion that you should be empowered to do concurrent things, without giving up safety. Also, Rust being a low-level language, it's about fearless concurrency without picking a specific implementation strategy. This means we must abstract over the strategy, to allow choice later, if we want to have any way to share code between users of different strategies.
关于Rust,一个值得注意的地方是fearless concurrency.。这是一个概念,你应该被授权做并行的事情,而不放弃安全。另外,Rust是一种低级语言,它是关于无畏的并发性而不是选择特定的实现策略。这意味着,如果我们想在不同策略的用户之间共享代码,我们必须对策略进行抽象,以便以后进行选择。
Futures abstract over computation. They describe the "what", independent of the "where" and the "when". For that, they aim to break code into small, composable actions that can then be executed by a part of our system. Let's take a tour through what it means to compute things to find where we can abstract.
Futures 抽象于计算。它们描述的是“what”,独立于“where”和“when”。为此,他们的目标是将代码分解成小的、可组合的操作,然后由系统的一部分执行。让我们来了解一下计算事物的意义,以便找到可以抽象的地方。
Send and Sync
Luckily, concurrent Rust already has two well-known and effective concepts abstracting over sharing between concurrent parts of a program: Send and Sync. Notably, both the Send and Sync traits abstract over strategies of concurrent work, compose neatly, and don't prescribe an implementation.
幸运的是,concurrent Rust已经有两个著名而有效的概念,它们抽象了程序并发部分之间的共享:Send和Sync。值得注意的是,Send和Sync特性都是对并发工作策略的抽象,组合得很整洁,并且没有规定实现。
As a quick summary:
简单总结一下:
-
Send
abstracts over passing data in a computation to another concurrent computation (let's call it the receiver), losing access to it on the sender side. In many programming languages, this strategy is commonly implemented, but missing support from the language side, and expects you to enforce the "losing access" behaviour yourself. This is a regular source of bugs: senders keeping handles to sent things around and maybe even working with them after sending. Rust mitigates this problem by making this behaviour known. Types can beSend
or not (by implementing the appropriate marker trait), allowing or disallowing sending them around, and the ownership and borrowing rules prevent subsequent access.
通过将计算中的数据传递给另一个并发计算来发送抽象(我们称之为接收方),从而在发送方失去对它的访问。在许多编程语言中,这种策略通常被实现,但缺少语言方面的支持,希望您自己强制执行“失去访问”行为。这是一个常见的bug源:发送者保存发送的东西的句柄,甚至可能在发送后使用它们。通过使这种行为为人所知,Rust 减轻了这个问题。可以发送或不发送类型(通过实现适当的标记特征),允许或不允许发送它们,并且所有权和借用规则阻止后续访问。 -
Sync
is about sharing data between two concurrent parts of a program. This is another common pattern: as writing to a memory location or reading while another party is writing is inherently unsafe, this access needs to be moderated through synchronisation.1 There are many common ways for two parties to agree on not using the same part in memory at the same time, for example mutexes and spinlocks. Again, Rust gives you the option of (safely!) not caring. Rust gives you the ability to express that something needs synchronisation while not being specific about the how.
同步是指在程序的两个并发部分之间共享数据。这是另一种常见的模式:由于在另一方写入数据时写入内存位置或读取数据本身是不安全的,因此需要通过同步来调节这种访问。双方有许多共同的方法同意不同时使用内存中的同一部分,例如互斥锁和自旋锁。同样,Rust给了你选择(安全!)不在乎。Rust让你有能力表达出某些东西需要同步,而不是具体的方式。
Note how we avoided any word like "thread", but instead opted for "computation". The full power of Send and Sync is that they relieve you of the burden of knowing what shares. At the point of implementation, you only need to know which method of sharing is appropriate for the type at hand. This keeps reasoning local and is not influenced by whatever implementation the user of that type later uses.
注意我们如何避免使用“thread”线程这样的词,而是选择“computation”计算。发送和同步的全部功能是它们减轻了您知道共享内容的负担。在实现时,您只需要知道哪种共享方法适合手头的类型。这使得推理保持局部性,并且不受该类型用户以后使用的任何实现的影响。
Send
and Sync
can be composed in interesting fashions, but that's beyond the scope here. You can find examples in the Rust Book.
发送和同步可以以有趣的方式组合,但这超出了本文的范围。你可以在Rust的书中找到例子。
To sum up: Rust gives us the ability to safely abstract over important properties of concurrent programs, their data sharing. It does so in a very lightweight fashion; the language itself only knows about the two markers Send and Sync and helps us a little by deriving them itself, when possible. The rest is a library concern.
总而言之:Rust让我们能够安全地抽象并发程序的重要属性,即它们的数据共享。它以一种非常轻量级的方式做到了这一点;语言本身只知道Send and Sync 这两个标记,如果可能的话,还可以通过派生它们来帮助我们。剩下的就是library 的事了。
An easy view of computation
While computation is a subject to write a whole book about, a very simplified view suffices for us: A sequence of composable operations which can branch based on a decision, run to succession and yield a result or yield an error
虽然计算是一个需要写一整本书的主题,但一个非常简单的视图就足够了:一个可组合的操作序列,它可以基于一个决策进行分支,运行到连续,并产生一个结果或产生一个错误
Deferring computation
As mentioned above, Send
and Sync
are about data. But programs are not only about data, they also talk about computing the data. And that's what Futures
do. We are going to have a close look at how that works in the next chapter. Let's look at what Futures allow us to express, in English. Futures go from this plan:
如上所述,发送和同步是关于数据的。但是程序不仅是关于数据的,它们还讨论如何计算数据。这就是Futures的作用。在下一章中,我们将仔细看看它是如何工作的。让我们来看看什么是Futures, 用英语来表达:Futures go from this plan:
- Do X
- If X succeeded, do Y
towards:
- Start doing X
- Once X succeeds, start doing Y
Remember the talk about "deferred computation" in the intro? That's all it is. Instead of telling the computer what to execute and decide upon now, you tell it what to start doing and how to react on potential events in the... well... Future.
还记得在介绍中提到的“延迟计算”吗?就是这样。不是告诉计算机现在执行什么和决定什么,而是告诉它开始做什么以及如何对潜在的事件做出反应。嗯…Future。
Orienting towards the beginning
Let's have a look at a simple function, specifically the return value:
让我们看看一个简单的函数,特别是返回值:
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)
}
You can call that at any time, so you are in full control on when you call it. But here's the problem: the moment you call it, you transfer control to the called function until it returns a value - eventually. Note that this return value talks about the past. The past has a drawback: all decisions have been made. It has an advantage: the outcome is visible. We can unwrap the results of the program's past computation, and then decide what to do with it.
你可以在任何时候调用它,所以你可以完全控制何时调用它。但问题是:在调用它的那一刻,您将控制权转移到被调用的函数,直到它返回一个值——最终。注意,这个返回值谈论的是过去。过去有一个缺点:所有的决定都已做出。它有一个优势:结果是可见的。我们可以打开程序过去计算的结果,然后决定如何处理它。
But we wanted to abstract over computation and let someone else choose how to run it. That's fundamentally incompatible with looking at the results of previous computation all the time. So, let's find a type that describes a computation without running it. Let's look at the function again:
但是我们想要抽象计算,让别人来选择如何运行它。这与一直查看以前的计算结果是根本不相容的。那么,让我们找一个类型来描述一个不运行它的计算。让我们再来看看这个函数:
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)
}
Speaking in terms of time, we can only take action before calling the function or after the function returned. This is not desirable, as it takes from us the ability to do something while it runs. When working with parallel code, this would take from us the ability to start a parallel task while the first runs (because we gave away control).
就时间而言,我们只能在调用函数之前或函数返回之后执行操作。这是不可取的,因为它剥夺了我们在运行时做某事的能力。当使用并行代码时,这将剥夺我们在第一次运行时启动并行任务的能力(因为我们放弃了控制权)。
This is the moment where we could reach for threads. But threads are a very specific concurrency primitive and we said that we are searching for an abstraction.
这是我们可以抓住 [threads]的时刻。但是 threads 是一个非常特殊的并发原语,我们正在寻找一个抽象。
What we are searching for is something that represents ongoing work towards a result in the future. Whenever we say "something" in Rust, we almost always mean a trait. Let's start with an incomplete definition of the Future trait:
我们正在寻找的是一种能够代表future不断取得成果的东西。每当我们在 Rust 中说 “something” 时,我们几乎总是指一种 trait。让我们从对 Future trait的不完全定义开始:
trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
Looking at it closely, we see the following:
仔细观察,我们可以发现:
- It is generic over the
Output
. //它在输出上是通用的。 - It provides a function called
poll
, which allows us to check on the state of the current computation. //它提供了一个名为poll的函数,允许我们检查当前计算的状态。 - (Ignore
Pin
andContext
for now, you don't need them for high-level understanding.) //(暂时忽略Pin和Context,高层理解不需要它们。)
Every call to poll()
can result in one of these two cases:
每次调用poll()都可能导致以下两种情况之一:
- The computation is done,
poll
will returnPoll::Ready
//计算完成,poll将返回poll::Ready - The computation has not finished executing, it will return
Poll::Pending
//计算尚未完成执行,它将返回Poll::Pending
This allows us to externally check if a Future
still has unfinished work, or is finally done and can give us the value. The most simple (but not efficient) way would be to just constantly poll futures in a loop. There are optimisations possible, and this is what a good runtime does for you. Note that calling poll
again after case 1 happened may result in confusing behaviour. See the futures-docs for details.
这使得我们可以从外部检查 Future 是否还有未完成的工作,或者是否最终完成,并能给我们 value。最简单(但不是最有效)的方法就是不断地在一个循环中 进行 poll futures。有一些优化是可能的,这就是一个好的运行时为您所做的。请注意,在案例1发生后再次调用poll可能会导致混淆行为。详情见 [futures-docs]。
Async
While the Future trait has existed in Rust for a while, it was inconvenient to build and describe them. For this, Rust now has a special syntax: async. The example from above, implemented with async-std, would look like this:
虽然 Future trait 已经在Rust 中存在了一段时间,但不方便 构建 和 描述它们。为此,Rust现在有一个特殊的语法:async。上面的示例是用async std实现的,如下所示:
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)
}
Amazingly little difference, right? All we did is label the function async and insert 2 special commands: .await.
惊人的小差别,对吧?我们所做的只是标记函数 async 并插入2个特殊命令:.await
This async function sets up a deferred computation. When this function is called, it will produce a Future<Output = io::Result<String>> instead of immediately returning a io::Result<String>. (Or, more precisely, generate a type for you that implements Future<Output = io::Result<String>>.)
此异步函数设置延迟计算。调用此函数时,它将生成未来的<Output=io::Result<String>>,而不是立即返回io::Result<String>。(或者,更准确地说,为您生成实现Future<Output=io::Result<String>>的类型。)
What does .await
do?
The .await postfix does exactly what it says on the tin: the moment you use it, the code will wait until the requested action (e.g. opening a file or reading all data in it) is finished. The .await? is not special, it's just the application of the ? operator to the result of .await. So, what is gained over the initial code example? We're getting futures and then immediately waiting for them?
.await 后缀的作用与它在tin上所说的完全一样:当您使用它时,代码将一直等到请求的操作(例如打开文件或读取其中的所有数据)完成。这个.await? 不是特别的,它只是 ? 在.await结果中的应用,那么,在最初的代码示例中获得了什么?我们得到了futures ,然后马上等他们?
The .await points act as a marker. Here, the code will wait for a Future to produce its value. How will a future finish? You don't need to care! The marker allows the component (usually called the “runtime”) in charge of executing this piece of code to take care of all the other things it has to do while the computation finishes. It will come back to this point when the operation you are doing in the background is done. This is why this style of programming is also called evented programming. We are waiting for things to happen (e.g. a file to be opened) and then react (by starting to read).
.await points 作为标记。在这里,代码将等待Future 产生其 value。Future 将如何结束?你不必在意!标记允许负责执行这段代码的组件(通常称为“运行时”)在计算完成时处理它必须做的所有其他事情。当您在后台执行的操作完成时,它将回到这一点。这就是为什么这种编程风格也称为事件编程。我们等待事情发生(例如,打开一个文件),然后做出反应(开始阅读)。
When executing 2 or more of these functions at the same time, our runtime system is then able to fill the wait time with handling all the other events currently going on.
当同时执行这些函数中的2个或更多时,我们的运行时系统就可以在等待时间内处理当前发生的所有其他事件。
Conclusion 结论
Working from values, we searched for something that expresses working towards a value available later. From there, we talked about the concept of polling.
从值开始工作,我们搜索一些表示朝着稍后可用的值工作的内容。在那里,我们讨论了polling的概念。
A Future is any data type that does not represent a value, but the ability to produce a value at some point in the future. Implementations of this are very varied and detailed depending on use-case, but the interface is simple.
Future 是不表示值的任何数据类型,而是在未来某个时间点产生值的能力。根据用例的不同,这方面的实现非常多样和详细,但是接口很简单。
Next, we will introduce you to tasks, which we will use to actually run Futures.
接下来,我们将向您介绍任务,我们将使用这些任务来实际运行 Futures。
1 Two parties reading while it is guaranteed that no one is writing is always safe.
双方在保证没有人写作的情况下阅读总是安全的。