【译】Async/Await(二)——Futures
原文标题:Async/Await
原文链接:https://os.phil-opp.com/async-await/#multitasking
公众号: Rust 碎碎念
翻译 by: Praying
Rust 中的 Async/Await
Rust 语言以 async/await 的形式对协作式多任务提供了最好的支持。在我们探讨 async/await 是什么以及它是怎样工作的之前,我们需要理解 future 和异步编程在 Rust 中是如何工作的。
Futures
future 表示一个可能还无法获取到的值。例如由另一个任务计算的整数或者从网络上下载的文件。future 不需要一直等待,直到值变为可用,而是可以继续执行直到需要这个值的时候。
示例
下面这个例子可以很好的阐述 future 的概念:
在这个时序图里,main
函数从文件系统读取一个文件,然后调用函数foo
。这个过程重复了两次:一次是调用同步的read_file
,另一次是调用异步的async_read_file
。
在同步调用的情况下,main
需要等待文件从文件系统载入。然后它才可以调用foo
函数,foo
又需要再次等待结果。
在调用异步的async_read_file
的情况下,文件系统直接返回一个 future 并且在后台异步地载入文件。这使得main
函数得以更加容易地调用foo
,foo
与文件载入并行运行。在这个例子中,文件载入在foo
返回之前就完成载入,所以main
可以直接对文件操作而不必等待foo
返回。
Rust 中的 Futures
在 Rust 中,future 通过Future[1] trait 来表示,它看起来像下面这样:
pub trait Future {
type Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>;
}
关联类型[2]Output
指定了异步的值的类型。例如,上图中的async_read_file
函数将会返回一个Future
实例,其中Output
类型被设置为File
。
poll[3]能够检查是否值已经可用。它返回一个Poll
枚举,看起来像下面这样:
pub enum Poll<T> {
Ready(T),
Pending,
}
当这个值可用时(例如,文件已经从磁盘上被完整地读取),该值会被包装在Ready
变量中然后被返回。否则,会返回一个Pending
变量,告诉调用者这个值目前还不可用。
poll
方法接收两个参数:self: Pin<&mut Self>
和 cx: &mut Context
。前者类似于一个普通的&mut self
引用,不同的地方在于Self
值被pinned[4]到它的内存位置。如果不理解 async/await 是如何工作的,就很难理解Pin
以及为什么需要它。因此,我们稍后再来解释这个问题。
cx: &mut Context
参数的目的是把一个Waker
实例传递给异步任务,例如从文件系统载入文件。Waker
允许异步任务发送通知表示任务(或任务的一部分)已经完成,例如文件已经从磁盘上载入。因为主任务知道当Future
就绪的时候自己会被提醒,所以它不需要一次又一次地调用poll
。在本文后面当我们实现自己的 Waker 类型时,我们将会更加详细地解释这个过程。
使用 Future(Working with Futures)
现在我们知道 future 是如何被定义的并且理解了poll
方法背后的基本思想。尽管如此,我们仍然不知道如何使用 future 来高效地工作。问题在于 future 表示异步任务的结果,而这个结果可能是不可用的。尽管如此,在实际中,我们经常需要这些值直接用于后面的计算。所以,问题是:我们怎样在我们需要时能够高效地取回一个 future 的值?
等待 Future
一个答案是等待 future 就绪。看起来类似下面这样:
let future = async_read_file("foo.txt");
let file_content = loop {
match future.poll(…) {
Poll::Ready(value) => break value,
Poll::Pending => {}, // do nothing
}
}
在这段代码里,我们通过在循环里一次又一次地调用poll
来等待 future。这里poll
的参数无关紧要,所以我们将其忽略。虽然这个方案能够工作,但是它非常低效,因为在该值可用之前 CPU 一直处于忙等待状态。
一个更加高效的方式是阻塞当前的线程直到 future 变为可用。当然这是在你有线程的情况下才有可能,所以这个解决方案对于我们的内核来讲不起作用,至少目前还不行。即使是在支持阻塞的系统上,这通常也是不希望发生的,因为它又一次地把一个异步任务转为了一个同步任务,从而抑制了并行任务潜在的性能优势。
Future 组合子(Future Combinators)
等待的一个替换选项是使用 future 组合子。Future 组合子是类似map
的方法,它们能够将 future 进行链接和组合,和Iterator
上的方法比较相似。这些组合子不是在 future 上等待,而是自己返回一个 future,这个 future 在poll
上进行了映射操作。
举个例子,一个简单的string_len
组合子,用于把Future<Output = String>
转换为Future<Output = usize>
,可能看起来像下面这样:
struct StringLen<F> {
inner_future: F,
}
impl<F> Future for StringLen<F> where F: Future<Output = String> {
type Output = usize;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> {
match self.inner_future.poll(cx) {
Poll::Ready(s) => Poll::Ready(s.len()),
Poll::Pending => Poll::Pending,
}
}
}
fn string_len(string: impl Future<Output = String>)
-> impl Future<Output = usize>
{
StringLen {
inner_future: string,
}
}
// Usage
fn file_len() -> impl Future<Output = usize> {
let file_content_future = async_read_file("foo.txt");
string_len(file_content_future)
}
这段代码不怎么有效,因为它没有处理pinning[5],但是这里它作为一个例子已经足够了。基本的思想是,string_len
函数把一个给定的Future
实例包装进一个新的StringLen
结构体,该结构体也实现了Future
。当被包装的 Future 被轮询(poll)时,它轮询内部的 future。如果这个值尚未就绪,被包装的 future 也会返回Poll::Pending
。如果这个值就绪,字符串会从Poll::Ready
变量中导出并且它的长度会被计算出来。之后,它会再次被包装进Poll::Ready
然后返回。
通过string_len
函数,我们可以在不必等待的情况下异步地计算一个字符串的长度。因为这个函数会再次返回一个Future
,所以调用者无法直接在返回值上操作,而是需要再次使用组合子函数。通过这种方式,整个调用图就变成了异步的,并且我们可以在某个时间点高效地同时等待多个 future,例如在 main 函数中。
手动编写组合子函数是困难的,因此它们通常由库来提供。然而 Rust 标准库本身没有提供组合子方法,但是半官方的(兼容no_std
)future
crate 提供了。它的FutureExt
trait 提供了高级别的组合子方法,像map
或者then
,这些组合子方法可以被用于操作带有任意闭包的结果。
优势
Future 组合子的最大优势在于,它们保持了操作的异步性。通过结合异步 I/O 接口,这种方式可以得到很高的性能。事实上,future 组合子实现为带有 trait 实现的普通结构体,这使得编译器能够对它们进行极度优化。如果想了解更多的细节,可以阅读Zero-cost futures in Rust[6]这篇文章,该文宣布了 future 加入了 Rust 生态系统。
缺点
尽管 future 组合子能够让我们写出非常高效的代码,但是在某些情况下由于类型系统和基于闭包的接口,使用它们也很困难。例如,考虑下面的代码:
fn example(min_len: usize) -> impl Future<Output = String> {
async_read_file("foo.txt").then(move |content| {
if content.len() < min_len {
Either::Left(async_read_file("bar.txt").map(|s| content + &s))
} else {
Either::Right(future::ready(content))
}
})
}
(在 playground 上尝试运行这段代码[7])
在这里,我们读取文件foo.txt
,接着使用then
组合子基于文件内容链接第二个 future。如果内容长度小于给定的min_len
,我们读取另一个文件bar.txt
然后使用map
组合子将其追加到content
中。否则,我们就仅返回foo.txt
的内容。
我们需要对传入then
里的闭包使用move
关键字,因为如果不这样做,将会出现一个关于min_len
的生命中周期错误。使用Either
包装器(wrapper)的原因是 if 和 else 语句块必须拥有相同的类型。因为我们在块中返回不同的 future 类型,所以我们必须使用包装器类型来把它们统一到相同类型。ready
函数把一个值包装进一个立即就绪的 future。需要这个函数是因为Either
包装器期望被包装的值实现了Future
。
正如你所想,对于较大的项目,这样写很快就能产生非常复杂的代码。如果涉及到借用和不同的生命周期,它会变得更为复杂。为此,我们投入了大量的工作来为 Rust 添加对 async/await 的支持,就是为了让异步代码的编写从根本上变得更加简单。
参考资料
Future: https://doc.rust-lang.org/nightly/core/future/trait.Future.html
[2]
关联类型: https://doc.rust-lang.org/book/ch19-03-advanced-traits.html#specifying-placeholder-types-in-trait-definitions-with-associated-types
[3]
poll: https://doc.rust-lang.org/nightly/core/future/trait.Future.html#tymethod.poll
[4]
pinned: https://doc.rust-lang.org/nightly/core/pin/index.html
[5]
pinning: https://doc.rust-lang.org/stable/core/pin/index.html
[6]
Zero-cost futures in Rust: https://aturon.github.io/blog/2016/08/11/futures/
[7]
在 playground 上尝试运行这段代码: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=91fc09024eecb2448a85a7ef6a97b8d8
【译】Async/Await(二)——Futures的更多相关文章
- [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)
[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...
- [译]async/await中使用阻塞式代码导致死锁
原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...
- [译]async/await中阻塞死锁
这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...
- [译]Async/Await - Best Practices in Asynchronous Programming
原文 避免async void async void异步方法只有一个目的:使得event handler异步可行,也就是说async void只能用于event handler. async void ...
- C#多线程和异步(二)——Task和async/await详解
一.什么是异步 同步和异步主要用于修饰方法.当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务 ...
- C#多线程和异步(二)——Task和async/await详解(转载)
一.什么是异步 同步和异步主要用于修饰方法.当一个方法被调用时,调用者需要等待该方法执行完毕并返回才能继续执行,我们称这个方法是同步方法:当一个方法被调用时立即返回,并获取一个线程执行该方法内部的业务 ...
- Koa2学习(二)async/await
Koa2学习(二)async/await koa2中用到了大量的async/await语法,要学习koa2框架,首先要好好理解async/await语法. async/await顾名思义是一个异步等待 ...
- C#线程学习笔记九:async & await入门二
一.异步方法返回类型 只能返回3种类型(void.Task和Task<T>). 1.1.void返回类型:调用方法执行异步方法,但又不需要做进一步的交互. class Program { ...
- 【译】Async/Await(三)——Aysnc/Await模式
原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...
随机推荐
- Python之Windows服务
1.首先要安装pywin32-220.win-amd64-py2.7.exe 2. SvcDoRun:服务启动的时候会执行的方法 SvcStop:服务停止的时候会执行的方法 # coding=utf- ...
- Python命令行模块(sys.argv,argparse,click)
Python作为一门脚本语言,经常作为脚本接受命令行传入参数,Python接受命令行参数大概有三种方式.因为在日常工作场景会经常使用到,这里对这几种方式进行总结. 命令行参数模块 这里命令行参数模块平 ...
- C# 好代码学习笔记(1):文件操作、读取文件、Debug/Trace 类、Conditional条件编译、CLS
目录 1,文件操作 2,读取文件 3,Debug .Trace类 4,条件编译 5,MethodImpl 特性 5,CLSCompliantAttribute 6,必要时自定义类型别名 目录: 1,文 ...
- 【软件测试 Python自动化】全网最全大厂面试题,看完以后你就是面试官!
前言 为了让大家更好的理解和学习投入到Python自动化来找到一份好的资料也是学习过程中,非常重要的一个点.你的检索能力越强,你就会越容易找到最合适你的资料. 有需要的小伙伴可以复制群号 313782 ...
- Docker 在搭建私有仓库配置镜像时候报错
今天搞私有镜像报了个错 ,看了,好久原来是 多了个空格 服务失败,因为控制进程退出时带有错误代码.参见"systemctl状态docker".详细信息参见"服务" ...
- Python处理邮件内容和提取邮件里的url地址
最近在搞一个邮箱验证账号注册和登录的模块.总结一下.就当记载.文章中涉及到域名和邮箱等都经过处理. 需求是这样子的,注册某个网站的账号,然后注册需要邮件内容激活,登录的时候如果不是常用设备的话也需要认 ...
- svn怎么上传文件
首先去网站下载TortoiseSVN,并安装 安装完后随便打开一个文件夹,如图,笔者在 E:\svn\ 文件下创建了一个simbo文件夹,选中并右键,出现了TortoiseSVN应用的选项,我们点 ...
- 【超级经典】程序员装B指南(转)
一.准备工作 "工欲善其事必先利其器." 1.电脑不一定要配置高,但是双屏是必须的,越大越好,能一个横屏一个竖屏更好.一个用来查资料,一个用来写代码.总之要显得信息量很大,效率 ...
- C语言输入字符串
首先强调一点,C语言没有字符串的概念!所谓的字符串实际上还是以数组形式保存的. 方法1 -- 通过"%s"输入 优点:简单明了,输入字符只要不大于数组长度都可以. #includ ...
- 主从同步遇到 Got fatal error 1236 from master when reading data from binary log: 'Could not find first log file name in binary log index file'时怎么解决
首先遇到这个是因为binlog位置索引处的问题,不要reset slave: reset slave会将主从同步的文件以及位置恢复到初始状态,一开始没有数据还好,有数据的话,相当于重新开始同步,可能会 ...