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

Fre*_*ios 12 syntax asynchronous future rust async-await

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

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

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

    result = task.Result;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

She*_*ter 24

你在混淆一些概念.

并发不能并行,与asyncawait对于工具的并发性,这可能有时意味着它们也是并行的工具.

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

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>, _ctx: &mut Context) -> Poll<Self::Output> {
            Poll::Ready(self.0 + self.1)
        }
    }

    Value(a, b)
}
Run Code Online (Sandbox Code Playgroud)

async fn long_running_operation(a: u8, b: u8) -> u8 {
    a + b
}
Run Code Online (Sandbox Code Playgroud)

请注意,"之前"代码基本上是今天功能实现poll_fn

另见彼得霍尔关于如何更好地跟踪许多变量的答案.

参考

关于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)
}
Run Code Online (Sandbox Code Playgroud)

这无法编译:

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...
Run Code Online (Sandbox Code Playgroud)

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
}
Run Code Online (Sandbox Code Playgroud)

这有效!

调用async函数不会运行任何操作

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

为什么不Future立即对创作进行调查?

有关最权威的答案,请在RFC pull请求中查看来自无船的评论:

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

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

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

本次讨论涵盖了Dart 2.0的一些背景知识:

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

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

  1. 在没有充分理由的情况下,总是屈服于性能损失.在大多数情况下,这并不重要,但有些确实如此.即使在你可以忍受它的情况下,在任何地方流血也是一种拖累.

  2. 总是屈服意味着使用async/await无法实现某些模式.特别是,像(伪代码)这样​​的代码是很常见的:

    getThingFromNetwork():
      if (downloadAlreadyInProgress):
        return cachedFuture
    
      cachedFuture = startDownload()
      return cachedFuture
    
    Run Code Online (Sandbox Code Playgroud)

    换句话说,您有一个异步操作,可以在完成之前多次调用.以后的呼叫使用相同的先前创建的待定未来.您希望确保不要多次启动操作.这意味着您需要在开始操作之前同步检查缓存.

    如果异步函数从一开始就是异步,则上述函数不能使用async/await.

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

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

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

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

cramert回复(请注意,此语法中的一些现已过时):

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

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

代码示例

这些示例使用1.31.0-nightly(2018-10-15)中的异步支持和期货预览包(0.3.0-alpha.7).

C#代码的文字转录

use futures; // 0.3.1

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);
    });
}
Run Code Online (Sandbox Code Playgroud)

如果你打电话foo,Rust中的事件序列将是:

  1. Future<Output = u8>返回实现的东西.

而已.尚未完成"实际"工作.如果您获取结果foo并将其推向完成(通过轮询,在这种情况下通过futures::executor::block_on),则接下来的步骤是:

  1. Future<Output = u8>从调用返回实现的东西long_running_operation(它还没有开始工作).

  2. another_operation 确实有效,因为它是同步的.

  3. .await宏导致的代码long_running_operation开始.在foo未来将继续返回"没有准备好",直到计算完成.

输出将是:

foo
another_operation
long_running_operation
Result: 3
Run Code Online (Sandbox Code Playgroud)

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

async

你也可以使用async块:

use futures::{future, FutureExt}; // 0.3.1

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
}
Run Code Online (Sandbox Code Playgroud)

在这里,我们将同步代码包装在一个async块中,然后等待这两个操作完成,然后才能完成此功能.

请注意,对于任何实际需要很长时间的事情来说,包装这样的同步代码并不是一个好主意.请参阅在将来-rs中封装阻塞I/O的最佳方法是什么?了解更多信息.

有线程池

// Requires the `thread-pool` feature to be enabled 
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
}
Run Code Online (Sandbox Code Playgroud)

  • 抱歉,目前还不清楚。您是否有一个Rust代码示例,其功能与我编写的C#代码相同?我的意思是:有2个与`async` /`await`异步运行的操作。 (2认同)

Pet*_*all 6

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

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));
Run Code Online (Sandbox Code Playgroud)

async/await形式上,那是:

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);
}
Run Code Online (Sandbox Code Playgroud)

它引入了大量的一次性使用的变量,可以说是比原来的版本与承诺差.那为什么要这么麻烦?

考虑这个变化,变量responseobjects稍后在计算中需要:

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);
}
Run Code Online (Sandbox Code Playgroud)

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

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)));
Run Code Online (Sandbox Code Playgroud)

每次需要返回前一个结果时,都需要将整个结构嵌套一层.这很快就会变得非常难以阅读和维护,但async/ await版本不会遇到这个问题.


Jas*_*rff 5

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

在 C# 和 JavaScript 中,async方法立即开始运行,无论await结果与否,它们都会被调度。在 Python 和 Rust 中,当你调用一个async方法时,在你调用它之前什么都不会发生(它甚至没有被调度)await。但无论哪种方式,它的编程风格都基本相同。

生成另一个任务(与当前任务并发并独立于当前任务运行)的能力由库提供:seeasync_std::task::spawntokio::task::spawn.


至于为什么Rustasync与 C# 不完全一样,好吧,考虑一下两种语言之间的差异:

  • Rust 不鼓励全局可变状态。在 C# 和 JS 中,每个async方法调用都隐式添加到全局可变队列中。这是某些隐式上下文的副作用。无论好坏,这都不是 Rust 的风格。

  • Rust 不是一个框架。C# 提供默认事件循环是有道理的。它还提供了一个很棒的垃圾收集器!许多在其他语言中成为标准的东西是 Rust 中的可选库。