Rust: [稳定] async/await MVP

创建于 2019-06-26  ·  58评论  ·  资料来源: rust-lang/rust

稳定目标: 1.38.0(beta cut 2019-08-15)

执行摘要

这是一个稳定最小可行 async/await 功能的提议,其中包括:

  • 函数和块上的async注释,导致它们在评估中被延迟,而是评估到未来。
  • await运算符,仅在async上下文中有效,它将未来作为参数并导致它所在的外部未来产生控制权,直到等待的未来完成。

相关先前讨论

RFC:

跟踪问题:

稳定:

达成重大决定

  • 异步表达式计算的未来是从其初始状态构造的,在产生之前不运行任何主体代码。
  • 异步函数的语法使用“内部”返回类型(与内部return表达式匹配的类型)而不是“外部”返回类型(对函数的调用求值的未来类型)
  • await 运算符的语法是“后缀点语法” expression.await ,而不是更常见的await expression或其他替代语法。

实施工作阻塞稳定

  • [x] async fns 应该能够接受多个生命周期 #56238
  • [x] 生成器的大小不应呈指数增长 #52924
  • [] async/await 功能的最小可行文档
  • [] 足够的编译器测试行为

未来的工作

  • 非标准上下文中的异步/等待: asyncawait目前依赖 TLS 来工作。 这是一个实现问题,不是设计的一部分,虽然它不会阻碍稳定性,但最终会得到解决。
  • 高阶异步函数: async作为闭包文字的修饰符在这里不稳定。 关于具有生命周期的异步闭包的捕获和抽象,需要更多的设计工作。
  • 异步特征方法:这涉及大量的设计和实现工作,但它是一个非常理想的特性。
  • 流处理: futures 库中 Future trait 的配对是 Stream trait,一个异步迭代器。 将支持操作流集成到 std 和语言中是一个理想的长期功能。
  • 优化生成器表示:可以做更多的工作来优化生成器的表示,使它们的大小更完美。 我们确保这严格来说是一个优化问题,在语义上没有意义。

背景

处理非阻塞 IO 对于开发高性能网络服务非常重要,这是 Rust 的一个目标用例,生产用户对此非常感兴趣。 出于这个原因,使用非阻塞 IO 编写服务使其符合人体工程学和可行的解决方案长期以来一直是 Rust 的目标。 async/await 功能是这项工作的结晶。

在 1.0 之前,Rust 有一个绿色线程系统,其中 Rust 提供了一个替代的、语言级线程原语,它构建在非阻塞 IO 之上。 然而,这个系统导致了几个问题:最重要的是引入了一个语言运行时,它甚至会影响没有使用它的程序的性能,显着增加了 FFI 的开销,并且有几个主要的未解决的设计问题与 greenthread 堆栈的实现有关.

在删除 greenthreads 之后,Rust 项目的成员开始研究基于期货抽象的替代解决方案。 有时也称为 promises,futures 在其他语言中作为非阻塞 IO 的基于库的抽象非常成功,并且众所周知,从长远来看,它们很好地映射到 async/await 语法,这可能使它们的方便性略低于一个完全不可见的greenthreading系统。

Future 抽象开发的主要突破是引入了基于轮询的期货模型。 其他语言使用基于回调的模型,其中 future 本身负责调度回调在完成时运行,而 Rust 使用基于轮询的模型,其中执行器负责轮询未来完成,而未来只是通知执行者它准备好使用 Waker 抽象来取得进一步的进展。 由于以下几个原因,该模型运行良好:

  • 它使 rustc 能够将 futures 编译为状态机,在大小和间接性方面都具有最小的内存开销。 与基于回调的方法相比,这具有显着的性能优势。
  • 它允许像执行器和反应器这样的组件作为库 API 存在,而不是语言运行时的一部分。 这避免了引入影响未使用此功能的用户的全局成本,并允许用户轻松替换其运行时系统的各个组件,而不是要求我们在语言级别为他们做出黑盒决策。
  • 它也创建了所有并发原语库,而不是通过 async 和 await 运算符的语义将并发烘焙到语言中。 这使得并发通过源文本变得更加清晰和可见,源文本必须使用可识别的并发原语来引入并发。
  • 它允许在没有开销的情况下取消,通过允许在完成之前删除正在执行的期货。 免费取消所有期货对执行程序和并发原语具有性能和代码清晰度方面的好处。

(最后两点也被认为是来自其他语言的用户的困惑来源,他们不正确,并带来了这些语言的期望。然而,这些属性都是基于轮询模型的不可避免的属性它具有其他明显的优势,并且在我们看来,一旦用户了解它们,就会成为有益的特性。)

然而,基于民意调查的模型在与引用交互时遇到了严重的人体工程学问题。 本质上,跨屈服点的引用引入了无法解决的编译错误,即使它们应该是安全的。 这导致了复杂、嘈杂的代码,充满了弧、互斥体和移动闭包,这些都不是绝对必要的。 即使把这个问题放在一边,如果没有语言级别的原语,future 也会迫使用户采用一种编写高度嵌套回调的风格。

出于这个原因,我们追求 async/await 语法糖,以支持跨屈服点正常使用引用。 在引入了Pin抽象后,可以安全地支持跨屈服点的引用,我们开发了一个原生的 async/await 语法,它将函数编译到我们基于轮询的期货中,允许用户获得异步 IO 的性能优势编写与标准命令式代码非常相似的代码。 最后一个特征是本稳定报告的主题。

异步/等待功能描述

async修饰符

关键字async可以应用于两个地方:

  • 在块表达式之前。
  • 在固有函数中的自由函数或关联函数之前。

_(异步函数的其他位置 - 例如闭包文字和特征方法,将在未来进一步开发并稳定。)_

async 修饰符通过“将其变成未来”来调整它所修改的项目。 在块的情况下,块被评估为它的结果的未来,而不是它的结果。 在函数的情况下,对该函数的调用返回其返回值的未来,而不是其返回值。 由 async 修饰符修改的项目内的代码称为处于异步上下文中。

async 修饰符通过使项目被评估为未来的纯构造函数来执行此修改,将参数和捕获作为未来的字段。 每个等待点都被视为该状态机的一个单独变体,并且未来的“轮询”方法根据用户编写的代码的转换在这些状态中推进未来,直到最终达到最终状态。

async move修饰符

与闭包类似,异步块可以将周围范围内的变量捕获到未来的状态中。 像闭包一样,这些变量默认是通过引用捕获的。 但是,它们可以通过值捕获,使用move修饰符(就像闭包一样)。 async出现在move ,使这些块async move { }块。

await运算符

在异步上下文中,可以使用以下语法通过将表达式与await运算符组合来形成新表达式:

expression.await

await 操作符只能在异步上下文中使用,并且它所应用的表达式的类型必须实现Future trait。 await 表达式的计算结果是它所应用的未来的输出值。

await 运算符产生对异步上下文评估的未来的控制,直到它应用到的未来完成。 这个让出控制的操作不能写在表面语法中,但如果可以(在这个例子中使用语法YIELD_CONTROL! ),await 的脱糖将大致如下所示:

loop {
    match $future.poll(&waker) {
        Poll::Ready(value)  => break value,
        Poll::Pending       => YIELD_CONTROL!,
    }
}

这允许您等待期货在异步上下文中完成评估,将通过Poll::Pending的控制权向外转发到最外层的异步上下文,最终转发到生成未来的执行程序。

主要决策点

立即屈服

我们的异步函数和块“立即产生”——构建它们是一个纯函数,在异步上下文主体中执行代码之前将它们置于初始状态。 在您开始轮询该未来之前,不会执行任何正文代码。

这与许多其他语言不同,在这些语言中,对异步函数的调用会立即开始工作。 在这些其他语言中,async 是一种固有的并发构造:当您调用 async 函数时,它会触发另一个任务开始与您当前的任务并发执行。 然而,在 Rust 中,futures 本身并不是以并发方式执行的。

我们可以让异步项在构造时执行到第一个等待点,而不是使它们纯。 然而,我们认为这更令人困惑:在构建 Future 或轮询期间执行代码取决于第一个 await 在主体中的位置。 在轮询期间而不是在构造期间执行所有代码的推理更简单。

参考:

返回类型语法

我们的异步函数的语法使用“内部”返回类型,而不是“外部”返回类型。 也就是说,他们说他们返回他们最终评估的类型,而不是说他们返回那个类型的未来。

在一个层面上,这是关于首选哪种清晰度的决定:因为签名还包含async注释,因此他们返回未来的事实在签名中明确表示。 但是,用户无需注意 async 关键字就可以看到该函数返回一个 future 会很有帮助。 但这也感觉像样板,因为信息也是由async关键字传达的。

真正让我们大吃一惊的是终生省略的问题。 任何异步函数的“外部”返回类型是impl Future<Output = T> ,其中T是内部返回类型。 但是,该未来本身也捕获任何输入参数的生命周期:这与 impl Trait 的默认值相反,除非您指定它们,否则不会假定捕获任何输入生命周期。 换句话说,使用外部返回类型意味着异步函数永远不会从生命周期省略中受益(除非我们做了一些更不寻常的事情,比如让生命周期省略规则对异步函数和其他函数的工作方式不同)。

我们决定考虑到外部返回类型实际上会写得多么冗长和坦率地令人困惑,所以不值得额外发出信号表明这会返回一个 future 以要求用户编写它。

析构函数排序

异步上下文中析构函数的顺序与非异步上下文中的相同。 确切的规则在这里有点复杂且超出范围,但一般来说,值超出范围时会被销毁。 但是,这意味着它们在使用后会继续存在一段时间,直到它们被清理干净。 如果该时间包含 await 语句,则需要将这些项目保留在将来的状态中,以便它们的析构函数可以在适当的时间运行。

作为对未来状态大小的优化,我们可以在某些或所有上下文中将析构函数重新排序为更早(例如,可以立即删除未使用的函数参数,而不是存储在未来的状态中)。 但是,我们决定不这样做。 析构函数的顺序对用户来说可能是一个棘手且令人困惑的问题,有时对程序语义非常重要。 我们选择放弃这种优化,以保证析构函数的顺序尽可能简单——如果所有 async 和 await 关键字都被删除,则析构函数的顺序相同。

(总有一天,我们可能会对将析构函数标记为纯的和可重新排序的方法感兴趣。这是未来的设计工作,也与 async/await 无关。)

参考:

等待运算符语法

与其他语言的 async/await 特性的一个主要偏差是我们的 await 运算符的语法。 这是大量讨论的主题,比我们在 Rust 设计中做出的任何其他决定都要多。

自 2015 年以来,Rust 有一个后缀?操作符用于符合人体工程学的错误处理。 早在 1.0 之前,Rust 也有一个用于字段访问和方法调用的后缀.操作符。 因为期货的核心用例是执行某种 IO,所以绝大多数期货评估为Result ,其中一些
某种错误。 这意味着实际上,几乎每个 await 操作都以?或之后的方法调用进行排序。 鉴于前缀和后缀运算符的标准优先级,这将导致几乎每个 await 运算符都写(await future)? ,我们认为这是非常不符合人体工程学的。

因此,我们决定使用后缀语法,它与?.运算符组合得非常好。 在考虑了许多不同的语法选项后,我们选择使用.运算符,后跟 await 关键字。

参考:

支持单线程和多线程执行器

Rust 旨在使编写并发和并行程序更容易,而不会增加编写在单线程上运行的程序的成本。 能够在单线程执行器和多线程执行器上运行异步函数非常重要。 这两个用例之间的主要区别在于,多线程执行程序将通过Send绑定它们可以生成的期货,而单线程执行程序则不会。

impl Trait语法的现有行为类似,异步函数“泄漏”它们返回的未来的自动特征。 也就是说,除了观察到外部返回类型是未来,调用者还可以根据对其主体的检查来观察该类型是 Send 还是 Sync。 这意味着当 async fn 的返回类型被调度到多线程执行器上时,它可以检查这是否安全。 但是,类型不需要是发送,因此单线程执行器上的用户可以利用性能更高的单线程原语。

有人担心,将异步函数扩展为方法时,这不会很好地工作,但经过一些讨论后确定情况不会有太大不同。

参考:

已知的稳定阻滞剂

状态大小

问题:#52924

目前实现到状态机的异步转换的方式根本不是最佳的,导致状态变得比必要的大得多。 因为状态大小实际上是超线性增长,所以当状态大小增长到大于正常系统线程的大小时,有可能触发实际堆栈上的堆栈溢出。 改进此代码生成器,使其大小更合理,至少不会严重到导致正常使用中的堆栈溢出,这是一个阻塞错误修复。

异步函数中的多个生命周期

问题:#56238

异步函数应该能够在它们的签名中具有多个生命周期,所有这些生命周期都在未来函数被调用时被“捕获”。 但是,当前在编译器内部降低到impl Future不支持多个输入生命周期; 需要更深层次的重构
这项工作。 因为用户很可能编写具有多个(可能全部被省略)输入生命周期的函数,所以这是一个阻塞错误修复。

其他阻塞问题:

标签

未来的工作

所有这些都是 MVP 的已知和非常高优先级的扩展,我们打算在发布 async/await 的初始版本后立即开始工作。

异步闭包

在最初的 RFC 中,我们还支持 async 修饰符作为闭包文字的修饰符,创建匿名异步函数。 但是,使用此功能的经验表明,在我们对稳定此用例感到满意之前,仍有许多设计问题需要解决:

  1. 变量捕获的性质在异步闭包中变得更加复杂,并且需要一些语法支持。
  2. 当前无法抽象具有输入生命周期的异步函数,并且可能需要一些额外的语言或库支持。

无性病支持

await 运算符的当前实现要求 TLS 在轮询内部未来时向下传递唤醒器。 这本质上是一种“hack”,使语法尽快在具有 TLS 的系统上工作。 从长远来看,我们无意承诺使用 TLS,而是更愿意将唤醒器作为普通函数参数传递。 但是,这需要对状态机生成代码进行更深入的更改,以便它可以处理参数。

尽管我们不会阻止实施此更改,但我们确实将其视为高优先级,因为它可以防止在没有 TLS 支持的系统上使用 async/await。 这是一个纯粹的实现问题:系统设计中没有任何内容需要使用 TLS。

异步特征方法

我们目前不允许在特征中使用异步相关的函数或方法; 这是唯一可以写fn而不是async fn 。 异步方法显然是一个强大的抽象,我们希望支持它们。

异步方法在功能上将被视为返回将实现未来的关联类型的方法; 每个异步方法都会为该方法转换成的状态机生成一个唯一的未来类型。

但是,由于该未来将捕获所有输入,因此也需要在该状态下捕获任何输入生命周期或类型参数。 这相当于一个称为泛型关联类型的概念,这是我们一直想要但尚未正确实现的功能。 因此,异步方法的解析与泛型关联类型的解析相关联。

还有一些突出的设计问题。 例如,异步方法是否可以与返回具有相同签名的未来类型的方法互换? 此外,异步方法在自动特征方面存在其他问题,因为当您使用异步方法对特征进行抽象时,您可能需要要求某些异步方法返回的未来实现自动特征。

一旦我们获得了这种最低限度的支持,对于未来的扩展还有其他设计考虑,例如使异步方法“对象安全”的可能性。

生成器和异步生成器

我们有一个不稳定的生成器特性,使用相同的协程状态机转换来获取产生多个值的函数并将它们转换为状态机。 此功能最明显的用例是创建编译为“迭代器”的函数,就像异步函数编译为
期货。 类似地,我们可以组合这两个特性来创建异步生成器——编译为“流”的函数,迭代器的异步等价物。 在网络编程中有非常明确的用例,这通常涉及在系统之间发送的消息流。

生成器有很多开放的设计问题,因为它们是一个非常灵活的功能,有很多可能的选择。 Rust 中生成器的最终设计在语法和库 API 方面仍然悬而未决且不确定。

A-async-await AsyncAwait-Focus F-async_await I-nominated T-lang disposition-merge finished-final-comment-period

最有用的评论

最后的征求意见期,有倾向合并,具体根据上述审查,现已完成

作为治理过程的自动化代表,我要感谢作者的工作以及所有做出贡献的人。

RFC 将很快合并。

所有58条评论

@rfcbot fcp 合并

团队成员@withoutboats已提议将其合并。 下一步是由其他标记的团队成员进行审查:

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • [x] @joshtriplett
  • [x] @nikomatsakis
  • []@pnkfelix
  • [x] @scottmcm
  • [x] @withoutboats

关注点:

一旦大多数审稿人批准(并且最多有 2 个未通过的批准),这将进入其最终评论期。 如果您发现在此过程中任何时候都没有提出的重大问题,请大声说出来!

有关标记的团队成员可以给我的命令的信息,请参阅此文档

(只需在上面的报告中注册现有的拦截器,以确保它们不会滑倒)

@rfcbot关注实现-工作-阻塞-稳定

团队成员...已提议合并

如何合并 Github问题(不是拉取请求)?

@vi机器人有点愚蠢,不检查它是问题还是 PR :) 您可以在此处将“合并”替换为“接受”。

哇,谢谢你的全面总结! 我只是在切切地关注,但我完全有信心你在一切之上。

@rfcbot审核

是否可以将“Triage AsyncAwait-Unclear 问题”明确添加到稳定阻止程序中(和/或对此表示关注)?

我有https://github.com/rust-lang/rust/issues/60414 ,我认为它很重要(显然,这是我的错误:p),并且希望至少在稳定之前明确推迟它:)

我只想向社区表示感谢 Rust 团队为此功能所做的努力! 有很多设计、讨论和沟通中的一些故障,但至少我,希望还有许多其他人,相信通过这一切,我们已经找到了 Rust 的最佳解决方案。 :tada:

(也就是说,我希望在未来的可能性中提到桥接基于完成和异步取消系统 API 的问题。TL;DR 他们仍然必须传递拥有的缓冲区。这是一个库问题,但是一个有提及。)

我还希望看到提及基于完成的 API 的问题。 (有关上下文,请参阅此内部线程)考虑到 IOCP 和io_uring引入,这可能会成为 Linux 上异步 IO 的方式,我认为有一个明确的方法来处理它们很重要。 IIUC 假设的异步删除想法不能安全地实现,并且传递拥有的缓冲区将不太方便并且性能可能较低(例如,由于更差的位置或由于额外的副本)。

@newpavlov我已经为 Fuchsia 实现了类似的东西,完全可以不用异步删除。 有几种不同的方法可以做到这一点,例如使用资源池,在这种情况下,获取资源可能必须等待一些清理工作在旧资源上完成。 当前的期货 API 可以并且已经被用于在生产系统中有效地解决这些问题。

但是,这个问题是关于 async/await 的稳定,这与已经稳定的期货 API 设计是正交的。 随时提出更多问题或打开有关 futures-rs 回购的讨论的问题。

@Ekleog

是否可以将“Triage AsyncAwait-Unclear 问题”明确添加到稳定阻止程序中(和/或对此表示关注)?

是的,这是我们每周都在做的事情。 WRT 那个特定问题 (#60414),我相信它很重要并且很乐意看到它被修复,但我们还没有能够决定它是否应该阻止稳定,特别是因为它已经在-> impl Trait观察到了

@cramertj谢谢! 我认为 #60414 的问题基本上是“现在错误可能会很快出现”,而-> impl Trait看起来之前甚至没有人注意到它——那么如果它被推迟也没关系,一些问题将不得不:)(FWIW 它出现在一个函数中的自然代码中,我在一个地方返回()并在另一个地方返回T::Assoc ,IIRC 使我无法编译它——不过,自从打开#60414 以来就没有检查过代码,所以也许我的记忆是错误的)

@Ekleog是的,这是有道理的! 我绝对可以理解为什么这会很痛苦——我创建了一个 zulip 流来更深入地研究这个特定问题。

编辑:没关系,我错过了1.38目标。

@cramertj

有几种不同的方法可以做到这一点,例如使用资源池,在这种情况下,获取资源可能必须等待一些清理工作在旧资源上完成。

与将缓冲区作为未来状态的一部分相比,它们不是效率较低吗? 我主要担心的是,当前的设计不会是零成本的(从某种意义上说,您将能够通过删除async抽象来创建更高效​​的代码),并且基于完成的 API 的人机工程学会更少,并且有没有明确的修复方法。 无论如何,它都不是一个表演障碍,但我认为重要的是不要忘记设计中的这些缺陷,因此要求在 OP 中提及它。

@公爵

lang 团队当然可以比我更好地判断这一点,但延迟到1.38以确保稳定的实现似乎更明智。

此问题针对 1.38,请参阅描述的第一行。

@huxi谢谢,我错过了。 编辑了我的评论。

@新巴甫洛夫

与将缓冲区作为未来状态的一部分相比,它们不是效率较低吗? 我主要担心的是,当前的设计不会是零成本的(从某种意义上说,您将能够通过删除异步抽象来创建更高效​​的代码)并且基于完成的 API 不太符合人体工程学,并且没有明确的修复方法它。 无论如何,它都不是一个表演障碍,但我认为重要的是不要忘记设计中的这些缺陷,因此要求在 OP 中提及它。

不,不一定,但让我们将此讨论移至单独线程上的问题,因为它与 async/await 的稳定性无关。

(也就是说,我希望在未来的可能性中提到桥接基于完成和异步取消系统 API 的问题。TL;DR 他们仍然必须传递拥有的缓冲区。这是一个库问题,但是一个有提及。)

我还希望看到提及基于完成的 API 的问题。 (有关上下文,请参阅此内部线程)考虑到 IOCP 和 io_uring 的引入,这可能会成为 Linux 上异步 IO 的方式,我认为有一个明确的方法来处理它们很重要。

我同意 Taylor 的观点,即在这个问题空间中讨论 API 设计将是题外话,但我确实想解决这些评论的一个特定方面(以及围绕 io_uring 的一般讨论)与 async/await 稳定相关的问题:定时。

io_uring 是今年2019RustCamp, Carl Lerche 谈到了他为什么在底层 IO 抽象 mio 中做出这个选择。 在 2016 年的这篇博文中, Aaron Turon 谈到了创建更高级别抽象的好处。 这些决定是很久以前做出的,如果没有它们,我们不可能达到现在的地步。

我们应该重新审视我们的基本期货模型的建议是我们应该回到 3 或 4 年前的状态,并从那时开始重新开始。 什么样的抽象可以覆盖基于完成的 IO 模型,而不会像 Aaron 描述的那样为更高级别的原语引入开销? 我们如何将该模型映射到允许用户像 async/await 那样编写“普通 Rust + 次要注释”的语法? 我们将如何处理将其集成到我们的内存模型中,就像我们对这些带有 pin 的状态机所做的那样? 试图为这些问题提供答案将是本主题的题外话; 关键是回答它们并证明答案是正确的,这就是工作。 到目前为止,不同贡献者之间长达十年的劳动年将不得不再次重做。

锈的目标是推出一款产品,人们可以使用,这意味着我们必须发货。 我们不能总是停下来展望明年可能会成为一件大事的未来,然后重新启动我们的设计过程以将其纳入其中。 我们会根据自己所处的情况尽力而为。 显然,感觉好像我们几乎没有错过一件大事可能会令人沮丧,但就目前而言,我们也无法全面了解 a) 最好的结果是什么处理 io_uring 将是,b) io_uring 在整个生态系统中的重要性。 我们不能基于此恢复 4 年的工作。

Rust 在其他领域已经存在类似的,甚至可能更严重的局限性。 我想强调去年秋天我和 Nick Fitzgerald 一起看过的一个:wasm GC 集成。 在 wasm 中处理托管对象的计划本质上是对内存空间进行分段,以便它们与非托管对象存在于一个单独的地址空间中(实际上,有一天在许多单独的地址空间中)。 Rust 的内存模型根本就不是为了处理单独的地址空间而设计的,今天处理堆内存的任何不安全代码都假设只有 1 个地址空间。 虽然我们已经勾勒出了破坏性和技术上非破坏性但极具破坏性的技术解决方案,但最有可能的前进道路是接受我们的 wasm GC 故事可能不是完美的最佳,因为我们正在处理 Rust 的局限性它存在。

我们在这里稳定的一个有趣的方面是我们正在从安全代码中提供自引用结构。 有趣的是,在Pin<&mut SelfReferentialGenerator> ,我们有一个指向整个生成器状态的可变引用(存储为Pin的字段),并且我们在该状态内有一个指针指向到另一片状态。 内部指针与可变引用别名

据我所知,可变引用并不用于实际访问指向另一个字段的指针所指向的内存部分。 (特别是,没有clone方法可以使用任何其他指针而不是自引用指针来读取指向字段的指针。)不过,这更接近于具有可变引用别名核心生态系统中的其他任何东西,尤其是 rustc 本身附带的任何东西。 我们在这里骑的“线”变得非常细,我们必须小心不要丢失我们想要基于可变引用进行的所有这些很好的优化。

很可能有一点我们可以在这一点上做这件事,尤其是因为Pin已经很稳定,但我觉得这是值得指出的是,这将显著复杂的任何规则走样允许其最终被和这不是。 如果您认为 Stacked Borrows 很复杂,请为事情变得更糟做好准备。

抄送https://github.com/rust-lang/unsafe-code-guidelines/issues/148

据我所知,可变引用并不用于实际访问指向另一个字段的指针所指向的内存部分。

人们已经讨论过让所有这些协程类型都实现Debug ,听起来这个对话还应该集成不安全的代码指南,以确保调试打印是安全的。

人们已经讨论过让所有这些协程类型都实现 Debug,听起来这个对话还应该集成不安全代码指南,以确保调试打印什么是安全的。

的确。 这样的Debug实现,如果它打印自引用字段,可能会禁止生成器内部的 MIR 级基于引用的优化。

关于拦截器的更新:

两个高级阻击者都取得了很大进展,实际上可能都完成了(?)。 来自@cramertj @tmandry@nikomatsakis 的更多信息会很棒:

  • 多生命周期问题应该已由 #61775 修复
  • 尺寸问题更加模棱两可; 总会有更多的优化要做,但我认为避免明显指数级增长的低挂果实已经大部分得到解决?

这使得文档和测试成为稳定此功能的主要障碍。 @Centril一直表示担心该功能没有经过充分测试或完善; @Centril是否有任何地方您列举了可以检查以推动此功能稳定的特定问题?

我不确定是否有人在驾驶文档。 任何想要专注于改进本书、参考资料等中的树内文档的人都会提供很棒的服务! 像期货回购或 areweasyncyet 中的树外文档有一些额外的时间。

截至今天,距离测试版结束还有 6 周的时间,所以假设我们有 4 周的时间(直到 8 月 1 日)完成这些事情,以确保我们不会下滑 1.38。

尺寸问题更加模棱两可; 总会有更多的优化要做,但我认为避免明显指数级增长的低挂果实已经大部分得到解决?

我相信是这样,还有一些最近也被关闭了; 但还有其他阻塞问题

@Centril是否有任何地方您列举了可以检查以推动此功能稳定的特定问题?

有一个 Dropbox 论文,其中列出了我们想要测试的内容,还有https://github.com/rust-lang/rust/issues/62121。 除此之外,我将尝试尽快重新审查我认为未充分测试的领域。 也就是说,某些领域现在已经得到很好的测试。

任何想要专注于改进本书、参考资料等中的树内文档的人都会提供很棒的服务!

的确; 我很乐意查看参考的 PR。 也抄送@ehuss。


我还想将async unsafe fn从 MVP 中移到它自己的功能门中,因为我认为 a) 它几乎没有用,b) 它没有经过特别好的测试,c) 它表面上的行为很奇怪,因为.await点不是您写unsafe { ... } ,这可以从“泄漏的实现 POV”中理解,但从效果 POV 中可以理解,d)它几乎没有讨论并且未包含在 RFC 中也没有这个报告,以及 e) 我们用const fn做了这个,它工作得很好。 (我可以写出特征门控 PR)

我可以破坏async unsafe fn稳定性,尽管我怀疑我们最终会采用与现在不同的设计。 但给我们时间来解决这个问题似乎是明智的!

我创建了https://github.com/rust-lang/rust/issues/62500以将async unsafe fn到一个不同的功能门并将其列为阻止程序。 我想我们也应该创建一个适当的跟踪问题。

我非常怀疑我们会为async unsafe fn达成不同的设计,并且对不将其包含在最初一轮稳定中的决定感到惊讶。 我写了一些不安全的async fn s,我想它们会使它们成为async fn really_this_function_is_unsafe()或其他东西。 这似乎是 Rust 用户在能够定义需要unsafe { ... }调用的函数方面的基本期望的回归。 另一个功能门将给人的印象是async / await未完成。

@cramertj似乎我们应该讨论一下! 我为它创建了一个 Zulip 主题,试图防止这个跟踪问题变得过于过载。

关于未来的大小,对影响每个await点的情况进行了优化。 我所知道的最后一个问题是#59087,其中在等待之前对未来的任何借用都可以使为该未来分配的大小增加一倍。 这是非常不幸的,但仍然比我们以前的位置好很多。

我知道如何解决这个问题,但除非这比我意识到的更常见,否则它可能不应该成为稳定 MVP 的障碍。

也就是说,我仍然需要查看这些优化对 Fuchsia 的影响(这已经被阻止了一段时间,但今天或明天应该会清除)。 我们很有可能会发现更多案例,并且需要决定是否应该阻止其中任何一个案例。

@cramertj (提醒:我确实使用 async/await 并希望它尽快稳定)您的论点听起来像是延迟 async/await 稳定的论点,而不是在没有适当的实验和思考的情况下立即稳定async unsafe的论点。

特别是因为它没有包含在 RFC 中,并且如果以这种方式强制退出,可能会触发另一个“参数位置中的 impl trait”。

[旁注,这里真的不值得讨论:对于“另一个功能门将导致 async/await 未完成的印象”,我每隔几个小时使用 async/await 就会发现一个错误,由少数人传播rustc 团队合法需要几个月来修复它们,这让我说它尚未完成。 最后一个是几天前修复的,我真的希望当我再次尝试用更新的 rustc 编译我的代码时不会发现另一个,但是…]

您的论点听起来像是延迟稳定 async/await 的论点,而不是在没有适当的实验和思考的情况下立即稳定不安全的异步。

不,这不是一个论据。 我相信async unsafe已经准备好了,无法想象任何其他设计。 我相信在这个初始版本中不包含它只会产生负面影响。 我不相信延迟async / await整体,也不相信async unsafe会产生更好的结果。

无法想象任何其他设计

另一种设计,虽然肯定需要复杂的扩展: async unsafe fnunsafe.await ,而不是call() 。 这背后的原因是在调用async fn并创建impl Future _没有任何不安全的事情可以做_。 这一步所做的就是将数据填充到一个结构中(实际上,所有async fn都是const来调用)。 不安全的实际点是用poll推进未来。

(恕我直言,如果unsafe是即时的,则unsafe async fn更有意义,如果unsafe延迟,则async unsafe fn更有意义。)

当然,如果我们永远没有办法说例如unsafe Future其中Future所有方法调用都不安全,那么“提升” unsafe以创建impl Future ,以及unsafe的契约是以安全的方式使用由此产生的未来。 但这也几乎可以在没有unsafe async fn下轻松完成,只需手动“脱糖”到async块: unsafe fn os_stuff() -> impl Future { async { .. } }

然而,最重要的是,是否真的存在一种方法可以让不变量在poll开始时需要保持,而无需在创建时保持。 在 Rust 中,您使用unsafe构造函数到安全类型(例如Vec::from_raw_parts )是一种常见模式。 但关键是构造后,类型_不能_被滥用; unsafe范围结束。 这种不安全的范围是 Rust 保证的关键。 如果您引入一个unsafe async fn来包装一个安全的impl Future并要求对其进行轮询的方式/时间,然后将其传递给安全代码,那么该安全代码会突然进入您的不安全屏障。 一旦你以任何方式使用这个未来而不是立即等待它,这_非常_可能会发生,因为它可能会通过_一些_外部组合器。

我猜 TL; DR 是肯定有async unsafe fn角落应该在稳定它之前进行适当的讨论,特别是在可能引入const Trait的方向(我有一个草稿博客发布关于将其推广到具有任何fn -modifying 关键字的“弱'效果'系统”)。 然而, unsafe async fn实际上可能已经足够清楚unsafe的“排序”/“定位”以稳定下来。

我相信基于效果的unsafe Future特性不仅超出了我们今天知道如何用语言或编译器表达的任何东西,而且由于额外的效果,它最终会是一个更糟糕的设计——它需要组合器具有的多态性。

在调用 async fn 并创建 impl Future 时,不能做任何不安全的事情。 这一步所做的就是将数据填充到一个结构中(实际上,所有异步 fn 都是要调用的常量)。 不安全的实际点是通过民意调查推进未来。

确实,由于async fn在被.await编辑之前不能运行任何用户代码,任何未定义的行为都可能会延迟到调用.await之前。 不过,我认为 UB 的点和unsafe ty 的点之间存在重要区别。 unsafe ty 的实际意义在于 API 作者决定用户需要承诺满足一组非静态可验证的不变量,即使违反这些不变量的结果不会导致 UB直到稍后在其他一些安全代码中。 一个常见的例子是unsafe函数来创建一个值,该值实现了具有安全方法的 trait(正是这个)。 我已经看到这用于确保例如Visitor -trait-implementing 类型(其实现依赖于unsafe不变量的实现)可以通过要求unsafe来构造类型而得到合理使用。 其他示例包括slice::from_raw_parts ,它本身不会导致 UB(类型有效性不变量除外),但对结果切片的访问会。

我不相信async unsafe fn代表一个独特的或有趣的案例——它遵循一个完善的模式,通过要求unsafe在安全接口后面执行unsafe行为构造函数。

@cramertj事实上,你甚至不得不为此争论(我并不是说我认为当前的解决方案是一个糟糕的解决方案,或者我有一个更好的主意)对我来说意味着这场辩论应该在关心 Rust 的人应该关注的地方:RFC 存储库。

作为提醒,引用自其自述文件:

如果 [...] ,您需要遵循此过程:

  • 对语言的任何语义或句法更改不是错误修复。
  • [...还有未引用的东西]

我并不是说当前的设计会发生任何变化。 实际上,考虑几分钟让我觉得这可能是我能想到的最好的设计。 但是过程使我们能够避免我们的信念成为 Rust 的危险,我们错过了许多遵循 RFC 存储库但没有阅读每一个问题的人的智慧,因为他们不遵循这里的过程。

有时不遵循该过程可能是有意义的。 在这里,我看不出有什么紧迫性可以保证为了避免 FCP 延迟大约 2 周而忽略该过程。

因此,请让 rust 对其社区诚实地了解它在自述文件中给出的承诺,并将该功能保持在功能门以下,直到至少有一个被接受的 RFC 并希望在野外更多地使用它。 无论是整个 async/await 功能门还是只是一个不安全的异步功能门,我都不在乎,但只是不要稳定一些(AFAIK)在 async-wg 之外几乎没有使用并且几乎不为人所知的东西整体社区。

我正在为这本书编写参考资料的第一遍。 一路上,我注意到 async-await RFC 说?运算符的行为尚未确定。 然而它似乎在异步块( playground )中工作正常。 我们应该把它移到一个单独的功能门吗? 还是在某个时候解决了? 我在稳定报告中没有看到它,但也许我错过了它。

(我也在Zulip 上问过这个问题

是的,它与returnbreakcontinue等的行为一起讨论和解决。 阿尔。 它们都做“唯一可能的事情”,并且表现得像在闭包内一样。

let f = unsafe { || {...} };也可以安全调用,而 IIRC 相当于将unsafe到闭包内部。
unsafe fn foo() -> impl Fn() { || {...} }

对我来说,这足以说明“离开unsafe范围后会发生不安全的事情”。

其他地方也是如此。 正如之前所指出的, unsafe并不总是潜在的 UB 所在。 例子:

    let mut vec: Vec<u32> = Vec::new();

    unsafe { vec.set_len(100); }      // <- unsafe

    let val = vec.get(5).unwrap();     // <- UB
    println!("{}", val);

对我来说,这似乎是对不安全的误解——不安全并不标志着“这里发生了不安全的操作”——它标志着“我保证我在这里坚持必要的不变量。” 虽然您可以在等待点维护不变量,因为它不涉及可变参数,但它不是检查您是否支持不变量的非常明显的站点。 它更有意义,并且与我们所有不安全抽象的工作方式更加一致,以保证您在调用站点维护不变量。

这与为什么将不安全视为一种效果会导致不准确的直觉有关(正如拉尔夫在去年首次提出该想法时所争论的那样)。 不安全特别是有意地不具有传染性。 虽然您可以编写调用其他不安全函数的不安全函数并将它们的不变量向上转发到调用堆栈,但这根本不是使用不安全的正常方式,它实际上是用于定义值契约和手动检查的语法标记你支持他们。

所以并不是每个设计决策都需要一个完整的 RFC,但我们一直在努力提供关于如何制定决策的更清晰和结构。 本期开篇的主要决策点清单就是一个例子。 使用我们可用的工具,我想围绕这个不安全的异步 fns 问题在结构化的共识点上进行尝试,所以这是一个带有投票的摘要帖子。

async unsafe fn

async unsafe fns 是只能在 unsafe 块内调用的异步函数。 在他们的身体内部被视为不安全的范围。 主要的替代设计是使 async unsafe fns 对await不安全,而不是调用。 有很多充分的理由选择不安全调用的设计:

  1. 它在语法上与非异步不安全 fns 的行为一致,调用也不安全。
  2. 它更符合不安全的一般工作方式。 不安全函数是一种抽象,它依赖于调用者所支持的一些不变量。 也就是说,它不是关于标记“不安全操作发生的地方”而是“保证不变量被维护的地方”的情况。 在实际指定参数的调用站点检查不变量是否得到维护比在等待站点(与选择和验证参数的时间分开)更明智。 这对于一般的不安全函数来说是非常正常的,它们通常会确定其他安全函数期望正确的某些状态
  3. 它更符合异步 fn 签名的脱糖概念,您可以将签名建模为等同于删除 async 修饰符并在将来包装返回类型。
  4. 替代方案在近期或中期(即几年)实施是不可行的。 没有办法在当前设计的 Rust 语言中创建一个不安全的未来。 某种“不安全的影响”将是一个巨大的变化,将产生深远的影响,需要处理它如何与今天已经存在的不安全(例如,正常的不安全函数和块)向后兼容。 添加 async unsafe fns 不会显着改变这种情况,而在当前对 unsafe 的解释下的 async unsafe fns 在近期和中期具有实际的实际用例。

@rfcbot问 lang“我们是否接受将异步不安全 fn 稳定化为调用不安全的异步 fn?”

我不知道如何使用 rfcbot 进行民意调查,但至少我已经提名了它。

团队成员@withoutboats已要求团队:T-lang,就以下问题达成共识:

“我们是否接受将异步不安全 fn 稳定化为调用不安全的异步 fn?”

  • [x] @Centril
  • [x] @cramertj
  • [x] @eddyb
  • []@joshtriplett
  • [x] @nikomatsakis
  • []@pnkfelix
  • [] @scottmcm
  • [x] @withoutboats

@没有船

我想围绕这个不安全的异步 fns 问题在结构化的共识点上进行尝试,所以这是一个带有民意调查的摘要帖子。

感谢您的撰写。 讨论让我确信async unsafe fn在今天每晚都有效。 (由于它看起来很稀疏,因此可能应该添加一些测试。)此外,您能否修改顶部的报告,其中包含报告的部分内容 + 对async unsafe fn行为方式的描述?

它更符合不安全的一般工作方式。 不安全函数是一种抽象,它依赖于调用者所支持的一些不变量。 也就是说,它不是关于标记“不安全操作发生的地方”而是“保证不变量被维护的地方”的情况。 在实际指定参数的调用站点检查不变量是否得到维护比在等待站点(与选择和验证参数的时间分开)更明智。 这对于一般的不安全函数来说是非常正常的,它们通常会确定其他安全函数期望正确的某些状态

作为一个不太关注的人,我同意并认为这里的解决方案是很好的文档。

我可能在这里不合时宜,但考虑到这一点

  • 期货本质上是组合的,它们是可组合的,这是最基本的。
  • 未来实现中的等待点通常是一个不可见的实现细节。
  • 未来与执行上下文非常遥远,实际用户可能介于两者之间而不是根。

在我看来,取决于特定等待使用/行为的不变量介于一个坏主意和不可能安全之间。

如果在某些情况下等待的输出值是维护不变量所涉及的,我认为未来可能只是一个需要不安全访问的包装器的输出,例如

struct UnsafeOutput<T>(T);
impl<T> UnsafeOutput<T> {
    unsafe fn unwrap(self) -> T { self.0 }
}

鉴于在这个“早期不安全”中unsafe ness 在async ness 之前,我会更高兴修饰符顺序为unsafe async fnasync unsafe fn ,因为unsafe (async fn)async (unsafe fn)更明显地映射到该行为。

我很乐意接受,但我强烈认为这里公开的包装顺序在外面有unsafe ,修饰符的顺序可以帮助清楚这一点。 ( unsafeasync fn的修饰符,而不是async的修饰符unsafe fn 。)

我很乐意接受,但我强烈认为这里公开的包装顺序在外面有unsafe ,修饰符的顺序可以帮助清楚这一点。 ( unsafeasync fn的修饰符,而不是async的修饰符unsafe fn 。)

我一直和你在一起,直到你最后一个括号点。 @withoutboats的文章让我很清楚,如果在调用站点处理不安全问题,您实际拥有的unsafe fn (恰好在异步上下文中调用)。

我想说我们粉刷了自行车棚async unsafe fn

我认为async unsafe fn更有意义,但我也认为我们应该在语法上接受 async、unsafe 和 const 之间的任何顺序。 但是async unsafe fn对我来说更有意义,因为您剥离异步并将返回类型修改为“脱糖”。

替代方案在近期或中期(即几年)实施是不可行的。 没有办法在当前设计的 Rust 语言中创建一个不安全的未来。

FWIW 当涉及到unsafe fn内部的闭包和函数特征时,我遇到了我在RFC2585 中提到的类似问题。 我没想到unsafe async fn会使用安全的poll方法返回Future ,而是使用unsafe返回UnsafeFuture unsafe投票方式。 (*) 当unsafe { }块内使用时,我们可以使.await也适用于UnsafeFuture s,但不是其他情况。

相对于我们今天所拥有的,这两个未来特征将是一个巨大的变化,并且它们可能会引入很多可组合性问题。 因此,探索替代品的船可能已经航行了。 特别是因为这与今天的Fn特征的工作方式不同(例如,我们没有UnsafeFn特征或类似的,我在RFC2585中的问题是在unsafe fn中创建闭包impls Fn()的闭包,也就是说,可以安全调用,即使这个闭包可以调用不安全的函数。

创建“不安全”的 Future 或闭包不是问题,问题在于在没有证明这样做是安全的情况下调用它们,特别是当它们的类型没有说必须这样做时。

(*) 我们可以为所有Future提供UnsafeFuture的全面实现,我们还可以提供UnsafeFuture一个unsafe方法来“解包”自身作为Futurepoll是安全的。

这是我的两分钱:

  • @cramertj的解释 (https://github.com/rust-lang/rust/issues/62149#issuecomment-510166207) 使我相信unsafe异步函数是正确的设计。
  • 我非常喜欢关键字unsafeasync的固定顺序
  • 我稍微喜欢unsafe async fn的排序,因为排序看起来更合乎逻辑。 类似于“快速电动汽车”与“电动快速汽车”。 主要是因为async fn脱糖为fn 。 因此,这两个关键字彼此相邻是有道理的。

我认为let f = unsafe { || { ... } }应该使f安全,永远不应该引入UnsafeFn特性,并且先验.await ing 和async unsafe fn应该是安全的。 任何UnsafeFuture需要强有力的理由!

所有这一切都是因为unsafe应该是明确的,而 Rust 应该将你推回到安全地带。 同样通过这个令牌, f...应该_not_ 是一个不安全的块,应该采用https://github.com/rust-lang/rfcs/pull/2585 ,并且一个async unsafe fn应该有一个安全的身体。

我认为最后一点可能相当关键。 有可能每个async unsafe fn都会使用一个unsafe块,但类似地,大多数会从一些安全分析中受益,而且许多听起来很复杂,很容易出错。

特别是在捕获闭包时,我们永远不应该绕过借用检查器。

所以我在这里的评论: https :

UnsafeFuture trait 会要求调用者写unsafe { }来轮询未来,但调用者不知道必须在那里证明哪些义务,例如,如果你得到一个Box<dyn UnsafeFuture> unsafe { future.poll() }安全吗? 对于所有期货? 你不可能知道。 所以这完全没用,正如@rpjohnst在不和谐中指出的类似UnsafeFn特征。

要求 Future 对 pol 始终安全是有道理的,而构建一个必须对 poll 安全的未来的过程可能是不安全的; 我想这就是async unsafe fn是什么。 但在这种情况下, fn项目可以记录需要维护的内容,以便返回的未来可以安全地轮询。

@rfcbot实现-工作-阻塞-稳定

据我所知,还有 2 个已知的实现阻塞器(https://github.com/rust-lang/rust/issues/61949,https://github.com/rust-lang/rust/issues/62517),它会仍然可以添加一些测试。 我正在解决我的问题,使 rfcbot 在时间上不是我们的拦截器,然后我们实际上会阻止修复。

@rfcbot解决实现-工作-阻塞-稳定

:bell: 根据上面的评论

最后的征求意见期,有倾向合并,具体根据上述审查,现已完成

作为治理过程的自动化代表,我要感谢作者的工作以及所有做出贡献的人。

RFC 将很快合并。

我们在这里稳定的一个有趣的方面是我们正在从安全代码中提供自引用结构。 有趣的是,在 Pin<&mut SelfReferentialGenerator> 中,我们有一个指向整个生成器状态的可变引用(存储为 Pin 中的字段),并且我们在该状态内有一个指向该状态另一部分的指针. 内部指针与可变引用别名!

作为对此的后续行动, @comex实际上设法编写了一些(安全的)异步 Rust 代码,这些代码违反了我们当前发出它们的方式

此页面是否有帮助?
0 / 5 - 0 等级