从 generator 的角度看 Rust 异步代码

2024-01-17 04:18

本文主要是介绍从 generator 的角度看 Rust 异步代码,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

0705efd23e9ad6125cd2906065230465.gif

文|Ruihang Xia

目前参与边缘时序数据存储引擎项目

本文 6992 字 阅读 18 分钟

作为 2018 edition 一个比较重要的特性 Rust 的异步编程现在已经得到了广泛的使用。使用的时候难免会好奇它是如何运作的,这篇文章尝试从 generator 以及变量捕获的方面进行探索,而后介绍了在嵌入式时序存储引擎 ceresdb-helix 的研发过程中遇到的一个场景。

囿于作者水平内容难免存在一些错漏之处,还烦请留言告知。

PART. 1

async/.await, coroutine and generator

async/.await 语法在 1.39 版本[1]进入 stable channel,它能够很方便地编写异步代码:

async fn asynchronous() {// snipped
}async fn foo() {let x: usize = 233;asynchronous().await;println!("{}", x);
}

在上面的示例中,局部变量 x 能够直接在一次异步过程(fn asynchoronous)之后使用,和写同步代码一样。而在这之前,异步代码一般是通过类似 futures 0.1[2] 形式的组合子来使用,想要给接下来 (如 and_then()) 的异步过程的使用的局部变量需要被显式手动地以闭包出入参的方式链式处理,体验不是特别好。

async/.await 所做的实际上就是将代码变换一下,变成 generator/coroutine[3] 的形式去执行。一个 coroutine 过程可以被挂起,去做一些别的事情然后再继续恢复执行,目前用起来就是 .await 的样子。以上面的代码为例,在异步过程 foo()中调用了另一个异步过程 asynchronous() ,在第七行的 .await 时当前过程的执行被挂起,等到可以继续执行的时候再被恢复。

而恢复执行可能需要之前的一些信息,如在 foo()中我们在第八行用到了之前的信息 x。也就是说 async 过程要有能力保存一些内部局部状态,使得它们能够在 .await 之后被继续使用。换句话说要在 generator state 里面保存可能在 yield 之后被使用的局部变量。这里需要引入 pin[4] 机制解决可能出现的自引用问题,这部分不再赘述。

PART. 2

visualize generator via MIR

我们可以透过 MIR[5]来看一下前面提到的 generator 是什么样子的。MIR 是 Rust 的一个中间表示,基于控制流图 CFG[6]表示。CFG 能够比较直观地展示程序执行起来大概是什么样子,MIR 在有时不清楚你的 Rust 代码到底变成了什么样子的时候能够起到一些帮助。

想要得到代码的 MIR 表示有几种方法,假如现在手边有一个可用的 Rust toolchain,可以像这样传递一个环境变量给 rustc ,再使用 cargo 进行构建来产生 MIR:

RUSTFLAGS="--emit mir" cargo build

构建成功的话会在 target/debug/deps/ 目录下生成一个 .mir 的文件。或者也能通过 https://play.rust-lang.org/ 来获取 MIR,在 Run 旁边的溢出菜单上选择 MIR 就可以。

由 2021-08 nightly 的 toolchain 所产生的 MIR 大概是这个样子的,有许多不认识的东西可以不用管,大概知道一下。

  • _0, _1 这些是变量

  • 有许多语法和 Rust 差不多,如类型注解,函数定义及调用和注释等就行了。

fn future_1() -> impl Future {let mut _0: impl std::future::Future; // return place in scope 0 at src/anchored.rs:27:21: 27:21let mut _1: [static generator@src/anchored.rs:27:21: 27:23]; // in scope 0 at src/anchored.rs:27:21: 27:23bb0: {discriminant(_1) = 0; // scope 0 at src/anchored.rs:27:21: 27:23_0 = from_generator::<[static generator@src/anchored.rs:27:21: 27:23]>(move _1) -> bb1; // scope 0 at src/anchored.rs:27:21: 27:23// mir::Constant// + span: src/anchored.rs:27:21: 27:23// + literal: Const { ty: fn([static generator@src/anchored.rs:27:21: 27:23]) -> impl std::future::Future {std::future::from_generator::<[static generator@src/anchored.rs:27:21: 27:23]>}, val: Value(Scalar(<ZST>)) }}bb1: {return; // scope 0 at src/anchored.rs:27:23: 27:23}
}fn future_1::{closure#0}(_1: Pin<&mut [static generator@src/anchored.rs:27:21: 27:23]>, _2: ResumeTy) -> GeneratorState<(), ()> {debug _task_context => _4; // in scope 0 at src/anchored.rs:27:21: 27:23let mut _0: std::ops::GeneratorState<(), ()>; // return place in scope 0 at src/anchored.rs:27:21: 27:23let mut _3: (); // in scope 0 at src/anchored.rs:27:21: 27:23let mut _4: std::future::ResumeTy; // in scope 0 at src/anchored.rs:27:21: 27:23let mut _5: u32; // in scope 0 at src/anchored.rs:27:21: 27:23bb0: {_5 = discriminant((*(_1.0: &mut [static generator@src/anchored.rs:27:21: 27:23]))); // scope 0 at src/anchored.rs:27:21: 27:23switchInt(move _5) -> [0_u32: bb1, 1_u32: bb2, otherwise: bb3]; // scope 0 at src/anchored.rs:27:21: 27:23}bb1: {_4 = move _2; // scope 0 at src/anchored.rs:27:21: 27:23_3 = const (); // scope 0 at src/anchored.rs:27:21: 27:23((_0 as Complete).0: ()) = move _3; // scope 0 at src/anchored.rs:27:23: 27:23discriminant(_0) = 1; // scope 0 at src/anchored.rs:27:23: 27:23discriminant((*(_1.0: &mut [static generator@src/anchored.rs:27:21: 27:23]))) = 1; // scope 0 at src/anchored.rs:27:23: 27:23return; // scope 0 at src/anchored.rs:27:23: 27:23}bb2: {assert(const false, "`async fn` resumed after completion") -> bb2; // scope 0 at src/anchored.rs:27:21: 27:23}bb3: {unreachable; // scope 0 at src/anchored.rs:27:21: 27:23}
}

这个 demo crate 中还有一些别的代码,不过对应上面的 MIR 的源码比较简单:

async fn future_1() {}

只是一个简单的空的异步函数,可以看到生成的 MIR 会膨胀很多,如果内容稍微多一点的话通过文本形式不太好看。我们可以指定一下生成的 MIR 的格式,然后将它可视化。

步骤大概如下:

RUSTFLAGS="--emit mir -Z dump-mir=F -Z dump-mir-dataflow -Z unpretty=mir-cfg" cargo build > mir.dot
dot -T svg -o mir.svg mir.dot

能够在当前目录下找到 mir.svg,打开之后可以看到一个像流程图的东西(另一幅差不多的图省略掉了,有兴趣的可以尝试通过上面的方法自己生成一份)

d1d43e96eb6edcdb0a525021d73e98db.png

这里将 MIR 按照基本单位 basic block (bb) 组织,原本的信息都在,并且将各个 basic block 之间的跳转关系画了出来。从上面的图中我们可以看到四个 basic blocks,其中一个是起点,另外三个是终点。首先起点的 bb0 switch(match in rust)了一个变量 _5,按照不同的值分支到不同的 blocks。能大概想象一下这样的代码:

match _5 {0: jump(bb1),1: jump(bb2),_ => unreachable()
}

而 generator 的 state 可以当成就是那个 _5,不同的值就是这个 generator 的各个状态。future_1 的状态写出来大概是这样

enum Future1State {Start,Finished,
}

如果是 §1 中的 async fn foo(),可能还会多一个枚举值来表示那一次 yield。此时再想之前的问题,就能够很自然地想到要跨越 generator 不同阶段的变量需要如何保存了。

enum FooState {Start,Yield(usize),Finished,
}

PART. 3

generator captured

让我们把保存在 generator state 中,能够跨越 .await/yield 被后续阶段使用的变量称为被捕获的变量。那么能不能知道到底哪些变量实际上被捕获了呢?让我们试一试,首先写一个稍微复杂一点的异步函数:

async fn complex() {let x = 0;future_1().await;let y = 1;future_1().await;println!("{}, {}", x, y);
}

生成的 MIR 及 svg 比较复杂,截取了一段放在了附录中,可以尝试自己生成一份完整的内容。

稍微浏览一下生成的内容,我们可以看到一个很长的类型总是出现,像是这样子的东西:

[static generator@src/anchored.rs:27:20: 33:2]
// or
(((*(_1.0: &mut [static generator@src/anchored.rs:27:20: 33:2])) as variant#3).0: i32)

对照我们代码的位置可以发现这个类型中所带的两个文件位置就是我们异步函数 complex()的首尾两个大括号,这个类型是一个跟我们这整个异步函数相关的类型。

通过更进一步的探索我们大概能猜一下,上面代码片段中第一行的是一个实现了 Generator trait[7] 的匿名类型(struct),而 "as variant#3" 是 MIR 中的一个操作,Projection 的 Projection::Downcast,大概在这里[8]生成。在这个 downcast 之后所做的 projection 的到的类型是我们认识的 i32。综合其他类似的片段我们能够推测这个匿名类型和前面描述的 generator state 是差不多的东西,而各个 variant 是不同的状态元组,投影这个 N 元组能够拿到被捕获的局部变量。

PART. 4

anchored

知道哪些变量会被捕获能够帮助我们理解自己的代码,也能够基于这些信息进行一些应用。

先提一下 Rust 类型系统中特殊的一种东西 auto trait[9] 。最常见的就是 Send 和 Sync,这种 auto trait 会自动为所有的类型实现,除非显式地用 negative impl opt-out,并且 negative impl 会传递,如包含了 !Send 的 Rc 结构也是 !Send 的。通过 auto trait 和 negative impl 我们控制一些结构的类型,并让编译器帮忙检查。

比如 anchored[10] crate 就是提供了通过 auto trait 和 generator 捕获机制所实现的一个小工具,它能够阻止异步函数中指定的变量穿过 .await 点。比较有用的一个场景就是异步过程中关于变量内部可变性的获取。

通常来说,我们会通过异步锁如 tokio::sync::Mutex 来提供变量的内部可变性;如果这个变量不会穿过 .await point 即被 generator state 捕获,那么 std::sync::Mutex 这种同步锁或者 RefCell 也能使用;如果想要更高的性能,避免这两者运行时的开销,那也能够考虑 UnsafeCell 或其他 unsafe 手段,但是就有一点危险了。而通过 anchored 我们可以在这种场景下控制不安全因素,实现一个安全的方法来提供内部可变性,只要将变量通过 anchored::Anchored 这个 ZST 进行标记,再给整个 async fn 带上一个 attribute 就能够让编译器帮我们确认没有东西错误地被捕获并穿越了 .await、然后导致灾难性的数据竞争。

就像这样:

#[unanchored]
async fn foo(){{let bar = Anchored::new(Bar {});}async_fn().await;
}

而这种就会导致编译错误:

#[unanchored]
async fn foo(){let bar = Anchored::new(Bar {});async_fn().await;drop(bar);
}

对于 std 的 Mutex, Ref 和 RefMut 等常见类型,clippy 提供了两个 lints[11] ,它们也是通过分析 generator 的类型来实现的。并且与 anchored 一样都有一个缺点,在除了像上面那样明确使用单独的 block 放置变量外,都会出现 false positive 的情况[12]。因为局部变量在其他的形式下都会被记录下来[13],导致信息被污染。

anchored 目前还缺少一些 ergonomic 的接口,attribute macro 和 ecosystem 的其他工具交互的时候也存在一点问题,欢迎感兴趣的小伙伴来了解一下 https://github.com/waynexia/anchored 

文档:

https://docs.rs/anchored/0.1.0/anchored/

「参 考」

[1]https://blog.rust-lang.org/2019/11/07/Async-await-stable.html

[2]https://docs.rs/futures/0.1.21/futures/

[3]https://github.com/rust-lang/rfcs/blob/master/text/2033-experimental-coroutines.md

[4]https://doc.rust-lang.org/std/pin/index.html

[5]https://blog.rust-lang.org/2016/04/19/MIR.html

[6]https://en.wikipedia.org/wiki/Control-flow_graph

[7]https://doc.rust-lang.org/std/ops/trait.Generator.html

[8]https://github.com/rust-lang/rust/blob/b834c4c1bad7521af47f38f44a4048be0a1fe2ee/compiler/rustc_middle/src/mir/mod.rs#L1915

[9]https://doc.rust-lang.org/beta/unstable-book/language-features/auto-traits.html

[10]https://crates.io/crates/anchored

[11]https://rust-lang.github.io/rust-clippy/master/#await_holding

[12]https://github.com/rust-lang/rust-clippy/issues/6353

[13]https://doc.rust-lang.org/stable/nightly-rustc/src/rustc_typeck/check/generator_interior.rs.html#325-334

   本周推荐阅读  

c2bce97c6253ffbb45e20db8db7a817e.png

应用运行时 Layotto 进入 CNCF 云原生全景图


a49548619ebb8942d524886fb35df66a.png

蚂蚁大规模 Kubernetes 集群无损升级实践指南【探索篇】


7ce0fb67cd8f934a01368fe206b7dea5.png

2021 大促 AntMonitor 总结 - 云原生 Prometheus 监控实践


bc9028e466393b23907abbc0a35b5b7b.png

云原生运行时的下一个五年

7f29227fcfe0de6dabba64f7a2d4635e.png

这篇关于从 generator 的角度看 Rust 异步代码的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/614854

相关文章

活用c4d官方开发文档查询代码

当你问AI助手比如豆包,如何用python禁止掉xpresso标签时候,它会提示到 这时候要用到两个东西。https://developers.maxon.net/论坛搜索和开发文档 比如这里我就在官方找到正确的id描述 然后我就把参数标签换过来

poj 1258 Agri-Net(最小生成树模板代码)

感觉用这题来当模板更适合。 题意就是给你邻接矩阵求最小生成树啦。~ prim代码:效率很高。172k...0ms。 #include<stdio.h>#include<algorithm>using namespace std;const int MaxN = 101;const int INF = 0x3f3f3f3f;int g[MaxN][MaxN];int n

计算机毕业设计 大学志愿填报系统 Java+SpringBoot+Vue 前后端分离 文档报告 代码讲解 安装调试

🍊作者:计算机编程-吉哥 🍊简介:专业从事JavaWeb程序开发,微信小程序开发,定制化项目、 源码、代码讲解、文档撰写、ppt制作。做自己喜欢的事,生活就是快乐的。 🍊心愿:点赞 👍 收藏 ⭐评论 📝 🍅 文末获取源码联系 👇🏻 精彩专栏推荐订阅 👇🏻 不然下次找不到哟~Java毕业设计项目~热门选题推荐《1000套》 目录 1.技术选型 2.开发工具 3.功能

代码随想录冲冲冲 Day39 动态规划Part7

198. 打家劫舍 dp数组的意义是在第i位的时候偷的最大钱数是多少 如果nums的size为0 总价值当然就是0 如果nums的size为1 总价值是nums[0] 遍历顺序就是从小到大遍历 之后是递推公式 对于dp[i]的最大价值来说有两种可能 1.偷第i个 那么最大价值就是dp[i-2]+nums[i] 2.不偷第i个 那么价值就是dp[i-1] 之后取这两个的最大值就是d

pip-tools:打造可重复、可控的 Python 开发环境,解决依赖关系,让代码更稳定

在 Python 开发中,管理依赖关系是一项繁琐且容易出错的任务。手动更新依赖版本、处理冲突、确保一致性等等,都可能让开发者感到头疼。而 pip-tools 为开发者提供了一套稳定可靠的解决方案。 什么是 pip-tools? pip-tools 是一组命令行工具,旨在简化 Python 依赖关系的管理,确保项目环境的稳定性和可重复性。它主要包含两个核心工具:pip-compile 和 pip

D4代码AC集

贪心问题解决的步骤: (局部贪心能导致全局贪心)    1.确定贪心策略    2.验证贪心策略是否正确 排队接水 #include<bits/stdc++.h>using namespace std;int main(){int w,n,a[32000];cin>>w>>n;for(int i=1;i<=n;i++){cin>>a[i];}sort(a+1,a+n+1);int i=1

【Rust练习】12.枚举

练习题来自:https://practice-zh.course.rs/compound-types/enum.html 1 // 修复错误enum Number {Zero,One,Two,}enum Number1 {Zero = 0,One,Two,}// C语言风格的枚举定义enum Number2 {Zero = 0.0,One = 1.0,Two = 2.0,}fn m

js异步提交form表单的解决方案

1.定义异步提交表单的方法 (通用方法) /*** 异步提交form表单* @param options {form:form表单元素,success:执行成功后处理函数}* <span style="color:#ff0000;"><strong>@注意 后台接收参数要解码否则中文会导致乱码 如:URLDecoder.decode(param,"UTF-8")</strong></span>

html css jquery选项卡 代码练习小项目

在学习 html 和 css jquery 结合使用的时候 做好是能尝试做一些简单的小功能,来提高自己的 逻辑能力,熟悉代码的编写语法 下面分享一段代码 使用html css jquery选项卡 代码练习 <div class="box"><dl class="tab"><dd class="active">手机</dd><dd>家电</dd><dd>服装</dd><dd>数码</dd><dd

生信代码入门:从零开始掌握生物信息学编程技能

少走弯路,高效分析;了解生信云,访问 【生信圆桌x生信专用云服务器】 : www.tebteb.cc 介绍 生物信息学是一个高度跨学科的领域,结合了生物学、计算机科学和统计学。随着高通量测序技术的发展,海量的生物数据需要通过编程来进行处理和分析。因此,掌握生信编程技能,成为每一个生物信息学研究者的必备能力。 生信代码入门,旨在帮助初学者从零开始学习生物信息学中的编程基础。通过学习常用