原文标题:Async/Await


原文链接:https://os.phil-opp.com/async-await/#multitasking

公众号: Rust 碎碎念


翻译 by: Praying

Pinning

在本文中我们已经与pinning偶遇多次。现在终于可以来讨论pinning是什么以及为什么需要它?

自引用结构体(Self-Referential Structs)

正如上面所解释的,状态机的变换把每个暂停点的局部变量存储在一个结构体中。对于像example函数这样的小例子,这会很直观且不会导致什么问题。但是,当变量开始互相引用时,事情就变得困难了。例如,考虑下面的函数:

async fn pin_example() -> i32 {
    let array = [1, 2, 3];
    let element = &array[2];
    async_write_file("foo.txt", element.to_string()).await;
    *element
}

这个函数创建了一个array,其中包含有123。它接着创建了一个对 array 最后一个元素的引用然后把它存入element变量。接下来,它把这个已经转换为字符串的数字异步地写入到文件foo.txt中。最后,它返回了被element引用的数字。

因为这个函数使用了一个的await操作,所以得到的状态机有三种状态:启动(start)、结束(end)和等待写入(waiting on write)。这个函数没有传入参数,所以开始状态的结构体是空的。和之前一样,结束状态也是空的因为函数在这个位置已经结束了。等待写入状态的结构体就比较有意思了:

struct WaitingOnWriteState {
    array: [1, 2, 3],
    element: 0x1001c, // address of the last array element
}

我们需要把arrayelement变量都存储起来,因为element在返回值的时候需要,而arrayelement所引用。element是一个引用,它存储了一个指向被引用元素的指针(也就是一个内存地址)。这里我们假设地址是0x1001c,在实际中,它需要是array的最后一个元素的地址,因此,它取决于结构体在内存中所处的位置。带有这样的内部指针的结构体被称为自引用(self-referential)结构体,因为它们通过自己的一个字段引用了它们自身。

自引用结构体的问题

我们的自引用结构体的内部指针导致了一个基本问题,当我们看到它的内存布局后,这个问题就会变得明显:

array字段的起始地址为0x10014element字段在地址0x10020。它指向了地址0x1001c,因为 array 的最后一个元素的位置就在这里。此时,一切都没有问题。但是,当我们试图把这个结构体移动到一个不同的内存地址时,问题就出现了:

我们把结构体往后移动了一下,因此现在它的起始地址为0x10024。当我们把结构体作为函数参数传递时或者把它赋值给另一个栈上的变量,就会发生这种情况。问题在于,element字段仍然指向地址0x1001c,而array的最后一个元素的地址已经变成0x1002c。因此,这个指针是悬垂(dangling)的,并会导致下一次调用poll时发生未定义行为。

可能的解决方案

解决这个悬垂指针问题有三种基本方式:

  • 在移动时更新指针:思路是无论什么时候,只要结构体在内存中被移动,就更新内部的指针,因此这个指针在移动后仍然是有效的。不幸的是,这种方式将会需要 Rust 作出很大的改变并且有可能导致巨大的性能开销。原因是,运行时需要追踪所有结构体字段的类型并且在每次移动操作时都要检查是否需要更新指针。

  • 存储一个偏移量来取代自引用:为了避免更新指针的需要,编译器可以把自引用存储为个结构体开始位置的偏移量。例如,上面的WaitingOnWriteState结构体中的element字段可以存储为值为 8 的element_offset字段。因为,引用指向的 array 里的元素起始于结构体开头的 8 字节。因为偏移位置在结构体移动时是不变的,所以不需要进行字段更新。

    这种方式的问题在于它需要编译器去探查所有的自引用。这在编译时是不可能实现的,因为一个引用的值可能取决于用户输入,因此,我们可能再次需要一个运行时系统来分析引用并正确地创建状态结构体。这不会导致运行时开销,但是也阻碍了特定的编译器优化,因此,它可能会再度引起巨大的性能开销。

  • 禁止移动结构体:正如我们上面所见,悬垂指针仅发生于我们在内存中移动结构体时,通过完全禁止在自引用结构体上的移动操作,可以避免这个问题。这种方式的一个显著优势在于,它可以在类型系统层面上被实现而不需要额外的运行时开销。缺点在于,它把处理可能是自引用结构的移动操作的负担交给了程序员。

因为要保证提供零成本抽象(zero cost abstraction)的原则,这意味着抽象不应该引入额外的运行时开销,所以 Rust 选择了第三种方案。也因此,pinningAPI 在RFC2349中被提出。接下来,我们将会对这个 API 进行简要介绍,并解释它是如何与 async/await 以及 future 一同工作的。

堆上的值(Heap Values)

第一个发现是,在大多数情况下,堆分配(heap allocated)的值已经在内存中有了一个固定地址。它们通过调用allocate来创建,然后被一个指针类型引用,比如Box<T>。尽管指针类型有可能被移动,但是指针指向的堆上的值仍然保持在相同的内存地址,除非它被一个deallocate调用来释放。

使用堆分配,我们可以尝试去创建一个自引用结构体:

fn main() {
    let mut heap_value = Box::new(SelfReferential {
        self_ptr: 0 as *const _,
    });
    let ptr = &*heap_value as *const SelfReferential;
    heap_value.self_ptr = ptr;
    println!("heap value at: {:p}", heap_value);
    println!("internal reference: {:p}", heap_value.self_ptr);
}

struct SelfReferential {
    self_ptr: *const Self,
}

在 playground 上运行代码

我们创建了一个名为SelfReferential的简单结构体,该结构体仅包含一个单独的指针字段。首先,我们使用一个空指针来初始化这个结构体,然后使用Box::new在堆上分配它。接着,我们计算出这个分配在堆上的结构体的内存地址并将其存储到一个ptr变量中。最后,我们通过把ptr变量赋值给self_ptr字段使得结构体成为自引用的。

当我们在 playground 上执行这段代码时,我们看到这个堆上的值的地址和它的内部指针的地址是相等的,这意味着,self_ptr字段是一个有效的自引用。因为heap_value只是一个指针,移动它(比如,把它作为参数传入函数)不会改变结构体自身的值,所以self_ptr在指针移动后依然是有效的。

但是,仍然有一种方式来破坏这个示例:我们可以摆脱Box<T>或者替换它的内容:

let stack_value = mem::replace(&mut *heap_value, SelfReferential {
    self_ptr: 0 as *const _,
});
println!("value at: {:p}", &stack_value);
println!("internal reference: {:p}", stack_value.self_ptr);

在 playground 上运行

这里,我们使用mem::replace函数使用一个新的结构体实例来替换堆分配的值。这使得我们把原始的heap_value移动到栈上,而结构体的self_ptr字段现在是一个仍然指向旧的堆地址的悬垂指针。当你尝试在 playground 上运行这个示例时,你会看到打印出的"value at:""internal reference:"这一行确实是输出的不同的指针。因此,在堆上分配一个值并不能保证自引用的安全。

出现上面的破绽的基本问题是,Box<T>允许我们获得堆分配值的&mut T引用。这个&mut引用让使用类似mem::replace或者mem::swap的方法使得堆上值失效成为可能。为了解决这个问题,我们必须阻止创建对自引用结构体的&mut引用。

Pin<Box>和 Unpin

pinning API 以Pin包装类型和Unpin标记 trait 的形式提供了一个针对&mut T问题的解决方案。这些类型背后的思想是对Pin的所有能被用来获得对 Unpin trait 上包装的值的&mut引用的方法(如get_mut或者deref_mut)进行管控。Unpin trait 是一个auto trait,它会为所有的类型自动实现,除了显式选择退出(opt-out)的类型。通过让自引用结构体选择退出Unpin,就没有(安全的)办法从一个Pin<Box<T>>类型获取一个&mut T。因此,它们的内部的自引用就能保证仍是有效的。

举个例子,让我们修改上面的SelfReferential类型来选择退出Unpin

use core::marker::PhantomPinned;

struct SelfReferential {
    self_ptr: *const Self,
    _pin: PhantomPinned,
}

我们通过添加一个类型为PhantomPinned_pin字段来选择退出。这个类型是一个零大小标记类型,它唯一目的就是不去实现Unpin trait。因为 auto trait 的工作方式,有一个字段不满足Unpin,那么整个结构体都会选择退出Unpin

第二步是把例子中的Box<SelfReferential>改为Pin<Box<SelfReferential>>类型。实现这个的最简单的方式是使用Box::pin函数,而不是使用Box::new创建堆分配的值。

let mut heap_value = Box::pin(SelfReferential {
    self_ptr: 0 as *const _,
    _pin: PhantomPinned,
});

除了把Box::new改为Box::pin之外,我们还需要在结构体初始化添加新的_pin字段。因为PhantomPinned是一个零大小类型,我们只需要它的类型名来初始化它。

当我们尝试运行调整后的示例时,我们看到它无法编译:

error[E0594]: cannot assign to data in a dereference of `std::pin::Pin<std::boxed::Box<SelfReferential>>`
  --> src/main.rs:10:5
   |
10 |     heap_value.self_ptr = ptr;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ cannot assign
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin<std::boxed::Box<SelfReferential>>`

error[E0596]: cannot borrow data in a dereference of `std::pin::Pin<std::boxed::Box<SelfReferential>>` as mutable
  --> src/main.rs:16:36
   |
16 |     let stack_value = mem::replace(&mut *heap_value, SelfReferential {
   |                                    ^^^^^^^^^^^^^^^^ cannot borrow as mutable
   |
   = help: trait `DerefMut` is required to modify through a dereference, but it is not implemented for `std::pin::Pin<std::boxed::Box<SelfReferential>>`

两个错误发生都是因为Pin<Box<SelfReferential>>类型没有实现DerefMut trait。这也正是我们想要的,因为DerefMut trait 将会返回一个&mut引用,这是我们想要避免的。发生这种情况是因为我们选择退出了Unpin并把Box::new改为了Box::pin

现在的问题在于,编译器不仅阻止了第 16 行的移动类型,还禁止了第 10 行的self_ptr的初始化。这会发生时因为编译器无法区分&mut引用的有效使用和无效使用。为了能够正常初始化,我们不得不使用不安全的get_unchecked_mut方法:

// safe because modifying a field doesn't move the whole struct
unsafe {
    let mut_ref = Pin::as_mut(&mut heap_value);
    Pin::get_unchecked_mut(mut_ref).self_ptr = ptr;
}

尝试在 playground 上运行

get_unchecked_mut函数作用于Pin<&mut T>而不是Pin<Box<T>>,所以我们不得不使用Pin::as_mut来对之前的值进行转换。接着,我们可以使用get_unchecked_mut返回的&mut引用来设置self_ptr字段。

现在,生下来的唯一的错误是mem::replace上的期望错误。记住,这个操作试图把一个堆分配的值移动到栈上,这将会破坏存储在self_ptr字段上的自引用。通过选择退出Unpin和使用Pin<Box<T>>,我们可以在编译期阻止这个操作,从而安全地使用自引用结构体。正如我们所见,编译器无法证明自引用的创建是安全的,因此我们需要使用一个不安全的块(block)并且确认其自身的正确性。

栈 Pinning 和 Pin<&mut T>

在先前的部分,我们学习了如何使用Pin<Box<T>>来安全地创建一个堆分配的自引用的值。尽管这种方式能够很好地工作并且相对安全(除了不安全的构造),但是需要的堆分配也会带来性能损耗。因为 Rust 一直想要尽可能地提供零成本抽象, 所以 pinning API 也允许去创建Pin<&mut T>实例指向栈分配的值。

不像Pin<Box<T>> 实例那样能够拥有被包装的值的所有权,Pin<&mut T>实例只是暂时地借用被包装的值。这使得事情变得更加复杂,因为它要求程序员自己确认额外的保证。最重要的是,一个Pin<&mut T> 必须在被引用的T的整个生命周期被保持 pinned,这对于栈上的变量很难确认。为了帮助处理这类问题,就有了像pin-utils这样的 crate。但是我仍然不会推荐 pinning 到栈上除非你真的知道自己在做什么。

想要更加深入地了解,请查阅pin 模块Pin::new_unchecked方法的文档。

Pinning 和 Futures

正如我们在本文中已经看到的,Future::poll方法以Pin<&mut Self>参数的形式来使用 pinning:

fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output>

这个方法接收self: Pin<&mut Self>而不是普通的&mut self,其原因在于,从 async/await 创建的 future 实例常常是自引用的。通过把Self包装进Pin并让编译器为由 async/await 生的自引用的 futures 选择退出Unpin,可以保证这些 futures 在poll调用之间在内存中不被移动。这就保证了所有的内部引用都是仍然有效的。

值得注意的是,在第一次poll调用之前移动 future 是没问题的。因为事实上 future 是懒惰的(lazy)并且直到它们被第一次轮询之前什么事情也不会做。生成的状态机中的start状态因此只包含函数参数,而没有内部引用。为了调用poll,调用者必须首先把 future 包装进Pin,这就保证了 future 在内存中不会再被移动。因为栈上的 pinning 难以正确操作,所以我推荐一直使用Box::pin组合Pin::as_mut

如果你想了解如何安全地使用栈 pinning 实现一个 future 组合字函数,可以去看一下map 组合子方法的源码,以及 pin 文档中的projections and structural pinning部分

【译】Async/Await(四)—— Pinning的更多相关文章

  1. [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)

    [译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...

  2. [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...

  3. [译]async/await中阻塞死锁

    这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的两篇博文中翻译过来. 原文1:Don'tBlock o ...

  4. [译]Async/Await - Best Practices in Asynchronous Programming

    原文 避免async void async void异步方法只有一个目的:使得event handler异步可行,也就是说async void只能用于event handler. async void ...

  5. How Javascript works (Javascript工作原理) (四) 事件循环及异步编程的出现和 5 种更好的 async/await 编程方式

    个人总结: 1.讲解了JS引擎,webAPI与event loop合作的机制. 2.setTimeout是把事件推送给Web API去处理,当时间到了之后才把setTimeout中的事件推入调用栈. ...

  6. 【译】Async/Await(三)——Aysnc/Await模式

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

  7. 深入理解协程(四):async/await异步爬虫实战

    本文目录: 同步方式爬取博客标题 async/await异步爬取博客标题 本片为深入理解协程系列文章的补充. 你将会在从本文中了解到:async/await如何运用的实际的爬虫中. 案例 从CSDN上 ...

  8. 【译】Async/Await(一)——多任务

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

  9. 【译】Async/Await(二)——Futures

    原文标题:Async/Await 原文链接:https://os.phil-opp.com/async-await/#multitasking 公众号: Rust 碎碎念 翻译 by: Praying ...

随机推荐

  1. Ubuntu系统下电脑驱动的安装(wifi无线网卡)

    今天给自己的笔记本电脑安装了新的Ubuntu 16.04但是安装之后发现wifi无法启用.这里特说明解决过程. 首先,网上的大部分教程是 选择"系统设置",点击"软件和更 ...

  2. niceyoo的2020年终总结-2021年Flag

    碎碎念,向本命年说再见! 又到了一年一度立 Flag 的时间了,怎么样,去年的 Flag 大家实现的怎么样?还有信心立下 2021 年的 Flag 吗~ 今年我算比较背的,年初的一次小意外,直接在床上 ...

  3. JavaDailyReports10_07

    动手动脑① 1 package test_1; 2 3 public class Test { 4 5 public static void main(String[] args) { 6 // TO ...

  4. LeapMotion控制器 java语言开发笔记--(LeapMotion控制器简介)

    (1)LeapMotion系统识别和追踪手,手指,以及根手指类似的工具,这个设备运行在一个极小的范围,这个范围拥有个高精度,高跟踪频率可以记录离散的点,手势,和动作. (2)LeapMotion控制器 ...

  5. haproxy 里的超时

    haproxy 中的超时 客户端请求阶段 timeout client haproxy 和客户端通信时,连接不活跃的时间,既不发送数据,也不ack接收的数据 如果未设置,则永不超时,此时连接是否超时依 ...

  6. MySQL 中的WAL机制

    本文主要概括的是 WAL 机制涉及的三种日志,具体与其他组件的联系.执行可查看 一条 sql 的执行过程详解 .MySQL中的事务原理和锁机制 . 是什么 WAL,全称是Write-Ahead Log ...

  7. Apache伪静态(Rewrite).htaccess文件详解

    Htaccess(超文本访问)是一个简单的配置文件,它允许设计师,开发者和程序员通过它来改变Apache Web服务器的配置.这些功能包括用户重定向.URL重写(url rewrite,国内很多称为伪 ...

  8. TensorFlow 基础概念

    初识TensorFlow,看了几天教程后有些无聊,决定写些东西,来夯实一下基础,提供些前进动力. 一.Session.run()和Tensor.eval()的区别: 最主要的区别就是可以使用sess. ...

  9. Lesson_strange_words4

    mount on 安装 arc 弧 actuator 马达,致动器:调节器 roughly 大致,大约 radially 径向,放射状 stepper 步进机 motor 电机,发动机 sequent ...

  10. 【Spring】Spring 入门

    Spring 入门 文章源码 Spring 概述 Spring Spring 是分层的 Java SE/EE 应用全栈式轻量级开源框架,以 IOC(Inverse Of Control,反转控制)和 ...