Rust Async: Pin概念解析
赖智超

Pin这个零抽象概念的引入重塑了rust生命周期借用检查的规则,是rust异步生态中极为关键的一环。然而其本身过于抽象,api过于生硬,即便是当时的不少rust官方人员在review 这部分api时也是一头雾水。要尽可能把这个概念讲清楚,本文先讲讲 Pin出现的历史背景和所需要解决的问题,具体的api后续再作解析。

async/await的实现机制

我们都知道rust在实现闭包时,编译器通过隐式地创建一个匿名的struct保存捕获到的变量,并对struct实现call方法来实现函数调用。 实现async/await函数时,由于需要记录当前所处的状态(每次await的时候都会导致一个状态),所以编译器往往生成的是一个匿名的enum,每个enum变体保存从外部或者之前的await点捕获的变量。 以如下代码为例:

fn main() {async fn func1() -> i32 { 12 }let func2 = async || -> i32 {let t = 1;                  let v = t + 1; let b = func1().await;let rv = &v;   *rv + b};let fut = func2();println!("future size: {}", std::mem::size_of_val(&fut));
}

从代码形式上看,好像 t, v是局部变量,运行时存储在stack中。然而由于await的存在,整个函数不再是一气呵成从头执行到尾,而是分成了两段。在执行第二段的时候, 前半段执行的局部变量已经从stack中清理掉了,而第二段捕获了第一段的局部变量 v, 因此 v只能保存在编译器生成的匿名enum中。这个enum充当了函数执行时的虚拟栈(virtual stack)。 如果将 letb=func1().await;和 letrv=&v;调换位置呢?从打印结果来看,生成的enum大小变大了,因为捕获的是 rv这个引用变量,而被引用的变量v也得一起保存在enum中, 也就是说借用一旦跨了await,就会导致编译器需要构造一个自引用的结构!

自引用结构

支持自引用结构是rust社区期待已久的特性,然而完美地支持却极具挑战,短时间内很难稳定。自引用结构类似下面的:

struct Foo {array: [Bar; 10],ptr : &'array Bar,
}

Foo作为一个整体,并没有借用外部的变量,因此具有static生命周期,然而内部ptr却借用了另一个field的元素。如果将一个 Foo的实例变量进行移动(memcpy整个结构),则移动后的ptr依然指向之前的地址,导致悬空指针。防止自引用变量被意外地移动是自引用需要解决的问题之一。

那是不是在支持async/await前得先稳定自引用特性呢?答案是不需要,因为async/await生成是匿名的自引用结构,用户无法直接读写结构内部的字段,因此只需要处理好意外移动的问题就可以。 防止意外移动的方案之前有人提出增加一个 Move marker trait,对于没有实现 Move的类型,编译器禁止类型的实例移动,这种方案涉及到编译器比较大的改动, 也增加了语言的复杂度。那能不能不动编译器,而只是在标准库里增加几个api的方式实现呢?事实上如果不想让一个 T类型的实例移动,只需要把它分配在堆上,用智能指针(如 Box)访问就行了, 因为移动 Box只是memcpy了指针,原对象并没有被移动。不过由于 Box提供的api中可以获取到 &mut T,进而可以通过 mem::swap间接将T移出。 所以只需要提供一个弱化版的智能指针api,防止泄露 &mut T就能够达到防止对象被移动。这就是实现 Pin api的主要思路。 Pin就像是一个铁笼子, 将自引用的猛兽关进去后,依然可以正常观察它,或者给它投点食物修改它,也可以把铁笼子移来移去,但不能把它放出来自由活动。

为啥Pin是零开销抽象?

既然为了防止对象移动,需要将其分配到堆上,需要额外的内存分配开销,怎么能称之为零开销的呢? 首先说明一个重要的特性:很多人会误认为调用带引用的async函数会生成自引用的对象,因此不能移动,这是不对的。async函数生成的匿名enum类似下面:

enum AsyncFuture {InitState(State0),Await1State(State1),Await2State(State2),//...
}

编译器生成的 AsyncFuture初始时是处于 InitState变体状态, State0只捕获了外部的变量,不存在自引用,因此可以自由移动和调用各种 future的组合子, 而只有将其提交到 executor中执行的时候, executor才会将状态推进到 Await1State等变体状态, State1及后续状态才会存在自引用的情况。 因此,使用 async构建异步逻辑时并不需要每处都进行内存分配,而是将异步逻辑构建成一整个task放进 executor的最后一步才需要内存分配。这是理解 Pin的关键。

其次,由于 executor本身通常需要执行各种不同的 Future,所以也意味着其处理的通常是 Box,也需要将 Future分配在堆上。因此 Pin的方式没有产生额外的开销。

Future组合子的生命周期限制

由于在safe rust中,使用Future组合子写代码没法构造自引用结构,所以接触过Futures 0.1版本的就应该清楚,要在组合子之间 传递数据非常麻烦,要么组合子通过传递 self, 然后又从 Future::Output传出来,要么把数据包装在 Arc中,使用引用计数共享,否则就会报生命周期错误,代码写起来很费劲, 不美观,同时也不够高效。 Pin这个概念的引入,使得rust代码在不使用 unsafe的前提下,支持编译器生成的自引用结构, async函数中可以从虚拟栈中借用数据,拓展了safe rust本身的表达能力。 没有Pin前写Future的api画风:

impl Socket {fn read(self, buf: Vec<u8>) ->impl Future<Item = (Self, Vec<u8>, usize), Error = (Self, Vec<u8>, io::Error)>;
}

有Pin的概念后:

fn read<'a>(&'a mut self, buf: &'a mut [u8]) -> impl Future<Item = usize, Error = io::Error> + 'a;

对应async函数的写法:

async fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error>;

总结

总结下Pin提出的主要思路:

在safe rust代码中写Future会因生命周期的限制,导致api复杂难用,等价的问题出现在async函数中引用变量不能跨越await;
分析发现其本质原因是因为这样会导致生成自引用结构;
自引用的rfc现在不完善,要在rust中完美支持自引用结构会是一个漫长的过程;
进一步分析发现编译器生成的enum是一个特例(结构是匿名的,内部字段不可直接访问,同时初始状态不包含自引用,可以自由移动);
不需要完美支持自引用,只需要保证自引用结构不可移动就能解决问题;
Pin概念提出并进入标准库,问题解决。

Rust Async: Pin概念解析相关推荐

  1. 四.MongoDB 概念解析

    1.MongoDB 概念解析 不管我们学习什么数据库都应该学习其中的基础概念,在mongodb中基本的概念是文档.集合.数据库,下面我们依次介绍. 下表将帮助您更容易理解Mongo中的一些概念: SQ ...

  2. 【深度学习】梯度和方向导数概念解析(代码基于Pytorch实现)

    [深度学习]梯度和方向导数概念解析(代码基于Pytorch实现) 文章目录 1 方向导数 2 梯度 3 自动求导实现 4 梯度下降4.1 概述4.2 小批量梯度下降 5 总结 1 方向导数 方向导数的 ...

  3. 【数字信号处理】相关系数 ( 相关系数概念解析 | 信号能量常数 | 共轭序列 | 序列在相同时刻的相关性 )

    文章目录 一.相关系数概念 二.相关系数概念解析 1.信号能量常数 2.共轭序列 3.序列在相同时刻的相关性 一.相关系数概念 " 相关系数 " 英文名称是 " Corr ...

  4. Webpack核心概念解析

    原文链接:banggan.github.io/2019/05/09/- Webpack核心概念解析 终于忙完了论文,可以愉快的开始学习了,重拾起重学前端.webpack以及Vue的源码解读作为入职前的 ...

  5. Apache Flink 零基础入门(一):基础概念解析

    Apache Flink 的定义.架构及原理 Apache Flink 是一个分布式大数据处理引擎,可对有限数据流和无限数据流进行有状态或无状态的计算,能够部署在各种集群环境,对各种规模大小的数据进行 ...

  6. 【Alljoyn】Alljoyn学习笔记五 AllJoyn开源技术基础概念解析

    AllJoyn开源技术基础概念解析 摘要: 总线(Bus) 实现P2P通信的基础 AllJoyn 的底层协议类似于D-Bus,相当于是跨设备分布式的 D-Bus 总线附件(Bus Attachment ...

  7. Java中堆、栈、常量池等概念解析

    Java中堆.栈.常量池等概念解析 程序运行时,我们最好对数据保存到什么地方做到心中有数.特别要注意的是内存的分配.有六个地方都可以保存数据: (1) 寄存器.这是最快的保存区域,因为它位于和其他所有 ...

  8. QUANT[6] 量化交易常见概念解析

    QUANT[1]:从零开始量化交易 - プロノCodeSteel - CSDN博客 QUANT[2]:量化交易策略基本框架搭建 - プロノCodeSteel - CSDN博客 QUANT[3]:量化交 ...

  9. 单片机编程软件很简单(22),keil单片机编程软件优化等级+概念解析

    单片机编程软件是单片机使用过程中不可缺少的一环,因此对于单片机编程软件,相关人员应当具备一定了解.往期文章中,小编对单片机编程软件有过诸多介绍.本文对于单片机编程软件的介绍基于两点:1.keil单片机 ...

  10. 固体物理半导体物理部分概念解析

    固体物理&半导体物理部分概念解析 固体物理部分 半导体物理部分 固体物理部分 半导体物理部分 简并和非简并 电子简并态&半导体简并态 简并(或者退化)系统也就是表现出显著量子效应的量子 ...

最新文章

  1. python画图代码彩虹-python绘制彩虹图
  2. 对C语言main函数中argc和argv[]的理解
  3. Python「八宗罪」
  4. shell 从1加到100
  5. 【解决】Authentication plugin 'caching_sha2_password' cannot be loaded
  6. php启用openssl,php怎么开启openssl模块
  7. C语言基础总结Part
  8. 计算机桌面图标的排列,如何进行桌面图标排列 让你的桌面一秒变酷炫【图文教程】...
  9. navicat premium相关应用(将oracle数据库迁移到mysql等)
  10. 没钱没资本可以创业不,想创业的人怎么办
  11. Python学习 :文件操作
  12. 数字IC设计工程师笔试面试经典题
  13. Anaconda安装python模块
  14. matlab 陈学松,基于强化学习的空调系统运行优化OPTIMIZATIONOF-同济大学.PDF
  15. geforce experience出现错误尝试重启PC
  16. swiper滑动切换变换样式,实时显示当前索引
  17. 专访丨互联网安全城市巡回赛冠军肖策:“大满贯”背后的秘密
  18. 《MATLAB专刊》——利用向量化编程提升MATLAB代码执行效率
  19. Map阶段环形缓冲区详细分析
  20. 解决(‘You must install pydot (`pip install pydot`) and install graphviz (see...) ‘, ‘for plot_model..

热门文章

  1. 使用$.getJSON解决ajax跨域访问 JQuery 的跨域方法(服务器端为wordpress程序)
  2. go语言多态接口样例
  3. 利用utl_smtp从oracle数据库发送带blob附件的电子邮件
  4. SQL 修改表字段失败 解决方法
  5. 【Linux】linux下解压.xz文件
  6. go语言项目如何引用依赖Github上的开源项目
  7. 再看《JavaScript高级程序设计》第8-9章
  8. wubi安裝ubuntukylin 14.04过程以及基本配置
  9. ostringstream的使用方法
  10. MapReduce - Map输入的分片