在Rust中async / await的目的是什么?

问题描述 投票:10回答:3

在像C#这样的语言中,给出这段代码(我没有故意使用await关键字):

async Task Foo()
{
    var task = LongRunningOperationAsync();

    // Some other non-related operation
    AnotherOperation();

    result = task.Result;
}

在第一行中,长操作在另一个线程中运行,并返回Task(这是未来)。然后,您可以执行另一个与第一个并行运行的操作,最后,您可以等待操作完成。我认为这也是async / await在Python,JavaScript等中的行为。

另一方面,在Rust,我在the RFC读到:

Rust的期货与其他语言的期货之间的根本区别在于,除非进行调查,否则Rust的期货不会做任何事情。整个系统是围绕这个建立的:例如,取消正在降低未来正是出于这个原因。相比之下,在其他语言中,调用异步fn会旋转一个立即开始执行的未来。

在这种情况下,Rust的async / await的目的是什么?看到其他语言,这种表示法是一种运行并行操作的便捷方式,但是如果调用async函数没有运行任何东西,我无法看到它在Rust中是如何工作的。

asynchronous syntax async-await rust future
3个回答
21
投票

你在混淆一些概念。

Concurrency is not parallelismasyncawait是并发工具,有时可能意味着它们也是并行工具。

另外,是否立即轮询未来与所选语法正交。

async / await

关键字asyncawait的存在使得创建和与异步代码交互更容易阅读,看起来更像“普通”同步代码。据我所知,在所有具有此类关键字的语言中都是如此。

更简单的代码

这是创建未来的代码,在轮询时添加两个数字

之前

fn long_running_operation(a: u8, b: u8) -> impl Future<Output = u8> {
    struct Value(u8, u8);

    impl Future for Value {
        type Output = u8;

        fn poll(self: Pin<&mut Self>, _lw: &LocalWaker) -> Poll<Self::Output> {
            Poll::Ready(self.0 + self.1)
        }
    }

    Value(a, b)
}

async fn long_running_operation(a: u8, b: u8) -> u8 {
    a + b
}

请注意,“之前”代码基本上是implementation of today's poll_fn function

另见Peter Hall's answer关于如何更好地跟踪许多变量。

参考

关于async / await的一个可能令人惊讶的事情是它能够实现以前无法实现的特定模式:使用期货中的参考。这是一些以异步方式填充缓冲区的代码:

之前

use std::io;

fn fill_up<'a>(buf: &'a mut [u8]) -> impl Future<Output = io::Result<usize>> + 'a {
    futures::future::lazy(move |_| {
        for b in buf.iter_mut() { *b = 42 }
        Ok(buf.len())
    })
}

fn foo() -> impl Future<Output = Vec<u8>> {
    let mut data = vec![0; 8];
    fill_up(&mut data).map(|_| data)
}

这无法编译:

error[E0597]: `data` does not live long enough
  --> src/main.rs:33:17
   |
33 |     fill_up_old(&mut data).map(|_| data)
   |                 ^^^^^^^^^ borrowed value does not live long enough
34 | }
   | - `data` dropped here while still borrowed
   |
   = note: borrowed value must be valid for the static lifetime...

error[E0505]: cannot move out of `data` because it is borrowed
  --> src/main.rs:33:32
   |
33 |     fill_up_old(&mut data).map(|_| data)
   |                 ---------      ^^^ ---- move occurs due to use in closure
   |                 |              |
   |                 |              move out of `data` occurs here
   |                 borrow of `data` occurs here
   |
   = note: borrowed value must be valid for the static lifetime...

use std::io;

async fn fill_up(buf: &mut [u8]) -> io::Result<usize> {
    for b in buf.iter_mut() { *b = 42 }
    Ok(buf.len())
}

async fn foo() -> Vec<u8> {
    let mut data = vec![0; 8];
    fill_up(&mut data).await.expect("IO failed");
    data
}

这有效!

Calling an async function does not run anything

另一方面,Future和围绕期货的整个系统的实现和设计与关键字asyncawait无关。事实上,在async / await关键字出现之前,Rust拥有蓬勃发展的异步生态系统(例如Tokio)。 JavaScript也是如此。

为什么Futures不会在创作时立即进行调查?

有关最权威的答案,请查看RFC pull请求中的this comment from withoutboats

Rust的期货与其他语言的期货之间的根本区别在于,除非进行调查,否则Rust的期货不会做任何事情。整个系统是围绕这个建立的:例如,取消正在降低未来正是出于这个原因。相比之下,在其他语言中,调用异步fn会旋转一个立即开始执行的未来。

关于这一点的一点是,Rust中的async和await不是固有的并发构造。如果您的程序只使用async&await且没有并发原语,程序中的代码将以定义的,静态已知的线性顺序执行。显然,大多数程序将使用某种并发方式在事件循环上调度多个并发任务,但它们不必这样做。这意味着你可以 - 平凡地 - 在本地保证某些事件的排序,即使在它们之间执行了非阻塞IO,你想要与一些更大的非本地事件异步(例如,你可以严格控制事件的顺序)在请求处理程序内部,同时与许多其他请求处理程序并发,甚至在等待点的两侧)。

这个属性为Rust的async / await语法提供了一种本地推理和低级控制,使Rust成为现实。运行到第一个等待点本身并不违反 - 你仍然知道代码执行的时间,它只会在两个不同的地方执行,具体取决于它是在await之前还是之后。但是,我认为其他语言立即开始执行的决定很大程度上源于他们的系统,当你调用异步fn时,它会立即同时安排任务(例如,这是我从Dart 2.0文档得到的潜在问题的印象) 。

Qazxswpoi涵盖了一些Dart 2.0背景:

嗨,我是Dart团队的成员。 Dart的async / await主要是由Erik Meijer设计的,他也在为异步/等待C#工作。在C#中,async / await与第一个await同步。对于Dart,Erik和其他人认为C#的模型太混乱,而是指定异步函数在执行任何代码之前总是产生一次。

当时,我和我的团队中的另一个人负责成为试验品,在我们的包管理器中尝试新的正在进行的语法和语义。根据这一经验,我们认为异步函数应该与第一个等待同步运行。我们的论点主要是:

  1. 在没有充分理由的情况下,总是屈服于性能损失。在大多数情况下,这并不重要,但有些确实如此。即使在你可以忍受它的情况下,在任何地方流血也是一种拖累。
  2. 始终屈服意味着使用async / await无法实现某些模式。特别是,像(伪代码)这样​​的代码是很常见的: this discussion from munificent 换句话说,您有一个异步操作,可以在完成之前多次调用。以后的呼叫使用相同的先前创建的待定未来。您希望确保不要多次启动操作。这意味着您需要在开始操作之前同步检查缓存。 如果异步函数从一开始就是异步,则上述函数不能使用async / await。

我们恳求我们的案例,但最终语言设计师坚持使用async-from-the-top。这是几年前的事了。

结果证明是错误的电话。性能成本是足够的,许多用户开发了“异步功能很慢”的思维模式,即使在性价比很高的情况下也开始避免使用它。更糟糕的是,我们看到令人讨厌的并发错误,人们认为他们可以在一个函数的顶部做一些同步工作,并且发现他们已经创造了竞争条件而感到沮丧。总的来说,似乎用户在执行任何代码之前不会自然地假设异步函数产生。

因此,对于Dart 2,我们现在正在进行非常痛苦的重大改变,以将异步函数更改为与第一个同步,并通过该转换迁移所有现有代码。我很高兴我们正在做出改变,但我真的希望我们在第一天做正确的事情。

我不知道Rust的所有权和性能模型是否会对你提出不同的限制,从顶部开始异步更好,但从我们的经验来看,同步到第一等待显然是Dart的更好的权衡。

getThingFromNetwork(): if (downloadAlreadyInProgress): return cachedFuture cachedFuture = startDownload() return cachedFuture (请注意,此语法中的一些现已过时):

如果您需要在调用函数时立即执行代码而不是稍后在轮询未来时执行代码,您可以编写如下函数:

cramert replies

代码示例

这些示例使用1.37.0-nightly(2019-06-05)中的异步支持和期货预览包(0.3.0-alpha.16)。

Literal transcription of the C# code

fn foo() -> impl Future<Item=Thing> {
    println!("prints immediately");
    async_block! {
        println!("prints when the future is first polled");
        await!(bar());
        await!(baz())
    }
}

如果你调用#![feature(async_await)] async fn long_running_operation(a: u8, b: u8) -> u8 { println!("long_running_operation"); a + b } fn another_operation(c: u8, d: u8) -> u8 { println!("another_operation"); c * d } async fn foo() -> u8 { println!("foo"); let sum = long_running_operation(1, 2); another_operation(3, 4); sum.await } fn main() { let task = foo(); futures::executor::block_on(async { let v = task.await; println!("Result: {}", v); }); } ,Rust中的事件序列将是:

  1. 返回实现foo的东西。

而已。尚未完成“实际”工作。如果你取Future<Output = u8>的结果并将其推向完成(通过轮询,在这种情况下通过foo),接下来的步骤是:

  1. 实现futures::executor::block_on的东西是从调用Future<Output = u8>返回的(它还没有开始工作)。
  2. long_running_operation确实有效,因为它是同步的。
  3. another_operation语法导致.await中的代码启动。在计算完成之前,long_running_operation的未来将继续“未准备好”。

输出将是:

foo

请注意,这里没有线程池:这都是在一个线程上完成的。

foo another_operation long_running_operation Result: 3 blocks

你也可以使用async块:

async

在这里,我们将同步代码包装在use futures::{future, FutureExt}; fn long_running_operation(a: u8, b: u8) -> u8 { println!("long_running_operation"); a + b } fn another_operation(c: u8, d: u8) -> u8 { println!("another_operation"); c * d } async fn foo() -> u8 { println!("foo"); let sum = async { long_running_operation(1, 2) }; let oth = async { another_operation(3, 4) }; let both = future::join(sum, oth).map(|(sum, _)| sum); both.await } 块中,然后等待这两个操作完成,然后才能完成此功能。

请注意,对于任何实际需要很长时间的事情来说,包装这样的同步代码并不是一个好主意。有关更多信息,请参阅async

With a threadpool

What is the best approach to encapsulate blocking I/O in future-rs?

6
投票

考虑这个简单的伪JavaScript代码,它获取一些数据,处理它,根据前一步获取更多数据,汇总它,然后打印结果:

use futures::{executor::ThreadPool, future, task::SpawnExt, FutureExt};

async fn foo(pool: &mut ThreadPool) -> u8 {
    println!("foo");

    let sum = pool
        .spawn_with_handle(async { long_running_operation(1, 2) })
        .unwrap();
    let oth = pool
        .spawn_with_handle(async { another_operation(3, 4) })
        .unwrap();

    let both = future::join(sum, oth).map(|(sum, _)| sum);

    both.await
}

getData(url) .then(response -> parseObjects(response.data)) .then(data -> findAll(data, 'foo')) .then(foos -> getWikipediaPagesFor(foos)) .then(sumPages) .then(sum -> console.log("sum is: ", sum)); 形式,那是:

async/await

它引入了许多一次性使用的变量,并且可能比使用promises的原始版本更糟糕。那为什么要这么麻烦?

考虑这种变化,稍后在计算中需要变量async { let response = await getData(url); let objects = parseObjects(response.data); let foos = findAll(objects, 'foo'); let pages = await getWikipediaPagesFor(foos); let sum = sumPages(pages); console.log("sum is: ", sum); } response

objects

并尝试使用promises以原始形式重写它:

async {
    let response = await getData(url);
    let objects = parseObjects(response.data);
    let foos = findAll(objects, 'foo');
    let pages = await getWikipediaPagesFor(foos);
    let sum = sumPages(pages, objects.length);
    console.log("sum is: ", sum, " and status was: ", response.status);
}

每次需要返回前一个结果时,都需要将整个结构嵌套一层。这很快就会变得非常难以阅读和维护,但getData(url) .then(response -> Promise.resolve(parseObjects(response.data)) .then(objects -> Promise.resolve(findAll(objects, 'foo')) .then(foos -> getWikipediaPagesFor(foos)) .then(pages -> sumPages(pages, objects.length))) .then(sum -> console.log("sum is: ", sum, " and status was: ", response.status))); / async版本不会遇到这个问题。


2
投票

Rust中await / async的目的是提供一个并发工具包 - 与C#和其他语言相同。

在C#和JavaScript中,await方法立即开始运行,并且无论你是否async,它们都会被安排。在Python和Rust中,当你调用await方法时,没有任何事情发生(它甚至没有安排),直到你async它。但无论如何,它的编程风格基本相同。

我认为你是正确的,产生另一个任务(与当前任务同时并独立运行)的能力是一个缺失的部分。也许它会被添加。 (记住,Rust的await尚未完成 - 设计仍在不断发展。)


至于为什么Rust async不完全像C#,那么考虑两种语言之间的差异:

  • Rust劝阻全球可变状态。在C#和JS中,每个async方法调用都隐式添加到全局可变队列中。这是一些隐含的背景的副作用。无论好坏,这不是Rust的风格。
  • Rust不是一个框架。 C#提供默认事件循环是有道理的。它还提供了一个伟大的垃圾收集器!其他语言标准的很多东西都是Rust中的可选库。
© www.soinside.com 2019 - 2024. All rights reserved.