Rust 中 async/await 的目的是什么? [英] What is the purpose of async/await in Rust?

查看:50
本文介绍了Rust 中 async/await 的目的是什么?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

In a language like C#, giving this code (I am not using the await keyword on purpose):

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

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

    result = task.Result;
}

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

In the first line, the long operation is run in another thread, and a Task is returned (that is a future). You can then do another operation that will run in parallel of the first one, and at the end, you can wait for the operation to be finished. I think that it is also the behavior of async/await in Python, JavaScript, etc.

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

On the other hand, in Rust, I read in the RFC that:

Rust 的期货与其他语言的期货之间的根本区别在于,除非轮询,否则 Rust 的期货不会做任何事情.整个系统都是围绕这一点构建的:例如,正是因为这个原因,取消正在放弃未来.相比之下,在其他语言中,调用 async fn 会启动一个立即开始执行的 future.

A fundamental difference between Rust's futures and those from other languages is that Rust's futures do not do anything unless polled. The whole system is built around this: for example, cancellation is dropping the future for precisely this reason. In contrast, in other languages, calling an async fn spins up a future that starts executing immediately.

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

In this situation, what is the purpose of async/await in Rust? Seeing other languages, this notation is a convenient way to run parallel operations, but I cannot see how it works in Rust if the calling of an async function does not run anything.

推荐答案

你混淆了几个概念.

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

Concurrency is not parallelism, and async and await are tools for concurrency, which may sometimes mean they are also tools for parallelism.

此外,是否立即轮询未来与选择的语法是正交的.

Additionally, whether a future is immediately polled or not is orthogonal to the syntax chosen.

关键字asyncawait 的存在是为了使异步代码的创建和交互更易于阅读,并且看起来更像普通"同步代码.据我所知,在所有具有此类关键字的语言中都是如此.

The keywords async and await exist to make creating and interacting with asynchronous code easier to read and look more like "normal" synchronous code. This is true in all of the languages that have such keywords, as far as I am aware.

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

This is code that creates a future that adds two numbers when polled

之前

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)
}

之后

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

请注意,before"代码基本上是 实现今天的poll_fn函数

Note that the "before" code is basically the implementation of today's poll_fn function

另请参阅Peter Hall 的回答,了解如何更好地跟踪许多变量.

See also Peter Hall's answer about how keeping track of many variables can be made nicer.

async/await 的一个潜在令人惊讶的事情是它启用了一种以前不可能的特定模式:在期货中使用引用.下面是一些以异步方式用值填充缓冲区的代码:

One of the potentially surprising things about async/await is that it enables a specific pattern that wasn't possible before: using references in futures. Here's some code that fills up a buffer with a value in an asynchronous manner:

之前

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
}

这有效!

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

The implementation and design of a Future and the entire system around futures, on the other hand, is unrelated to the keywords async and await. Indeed, Rust has a thriving asynchronous ecosystem (such as with Tokio) before the async / await keywords ever existed. The same was true for JavaScript.

有关最权威的答案,请查看此评论来自 withoutboats 关于 RFC 拉取请求:

For the most authoritative answer, check out this comment from withoutboats on the RFC pull request:

Rust 的期货与其他期货的根本区别语言是,除非轮询,否则 Rust 的期货不会做任何事情.这整个系统都是围绕这个构建的:例如,取消是正是因为这个原因放弃了未来.相比之下,在其他语言,调用异步 fn 会启动一个开始执行的未来立即.

A fundamental difference between Rust's futures and those from other languages is that Rust's futures do not do anything unless polled. The whole system is built around this: for example, cancellation is dropping the future for precisely this reason. In contrast, in other languages, calling an async fn spins up a future that starts executing immediately.

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

A point about this is that async & await in Rust are not inherently concurrent constructions. If you have a program that only uses async & await and no concurrency primitives, the code in your program will execute in a defined, statically known, linear order. Obviously, most programs will use some kind of concurrency to schedule multiple, concurrent tasks on the event loop, but they don't have to. What this means is that you can - trivially - locally guarantee the ordering of certain events, even if there is nonblocking IO performed in between them that you want to be asynchronous with some larger set of nonlocal events (e.g. you can strictly control ordering of events inside of a request handler, while being concurrent with many other request handlers, even on two sides of an await point).

这个属性给了 Rust 的 async/await 语法一种本地的推理与使 Rust 成为现实的低级控制.跑起来到第一个等待点不会本质上违反这一点 - 你会仍然知道代码何时执行,它只会执行两次不同的地方取决于它是在之前还是之后等待.但是,我认为其他语言做出的决定开始立即执行很大程度上源于他们的系统当您调用 async fn 时立即同时调度任务(例如,这是我得到的潜在问题的印象来自 Dart 2.0 文档).

This property gives Rust's async/await syntax the kind of local reasoning & low-level control that makes Rust what it is. Running up to the first await point would not inherently violate that - you'd still know when the code executed, it would just execute in two different places depending on whether it came before or after an await. However, I think the decision made by other languages to start executing immediately largely stems from their systems which immediately schedule a task concurrently when you call an async fn (for example, that's the impression of the underlying problem I got from the Dart 2.0 document).

Dart 2.0 的一些背景在 这个讨论中涵盖:

Some of the Dart 2.0 background is covered by this discussion from munificent:

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

Hi, I'm on the Dart team. Dart's async/await was designed mainly by Erik Meijer, who also worked on async/await for C#. In C#, async/await is synchronous to the first await. For Dart, Erik and others felt that C#'s model was too confusing and instead specified that an async function always yields once before executing any code.

当时,我和我团队中的另一个人的任务是成为豚鼠来尝试我们的新的正在进行的语法和语义包管理器.基于那次经验,我们觉得异步函数应该与第一个等待同步运行.我们的论点是主要是:

At the time, I and another on my team were tasked with being the guinea pigs to try out the new in-progress syntax and semantics in our package manager. Based on that experience, we felt async functions should run synchronously to the first await. Our arguments were mostly:

  1. 总是让步一次会无缘无故地导致性能损失.在大多数情况下,这无关紧要,但在某些情况下确实如此做.即使在你可以忍受它的情况下,流血也是一种拖累无处不在的小性能.

  1. Always yielding once incurs a performance penalty for no good reason. In most cases, this doesn't matter, but in some it really does. Even in cases where you can live with it, it's a drag to bleed a little perf everywhere.

总是让步意味着某些模式无法使用 async/await 实现.特别是,像这样的代码真的很常见(此处为伪代码):

Always yielding means certain patterns cannot be implemented using async/await. In particular, it's really common to have code like (pseudo-code here):

getThingFromNetwork():
  if (downloadAlreadyInProgress):
    return cachedFuture

  cachedFuture = startDownload()
  return cachedFuture

换句话说,你有一个异步操作,你可以在它完成之前多次调用它.后来的调用使用相同的先前创建的未决未来.你想确保你没有开始多次操作.这意味着你需要同步开始操作前检查缓存.

In other words, you have an async operation that you can call multiple times before it completes. Later calls use the same previously-created pending future. You want to ensure you don't start the operation multiple times. That means you need to synchronously check the cache before starting the operation.

如果 async 函数从一开始就是 async,上面的函数就不能使用 async/await.

If async functions are async from the start, the above function can't use async/await.

我们为自己的案子辩护,但最终语言设计者坚持了从顶部异步.这是几年前的事了.

We pleaded our case, but ultimately the language designers stuck with async-from-the-top. This was several years ago.

结果证明这是错误的调用.性能成本是真实的足以让许多用户形成一种心态,即异步函数是慢"并开始避免使用它,即使在性能达到的情况下是负担得起的.更糟糕的是,我们看到令人讨厌的并发错误,人们认为他们可以在函数顶部做一些同步工作很沮丧地发现他们创造了竞争条件.总的来说,它似乎用户不会自然地假设异步函数之前会产生执行任何代码.

That turned out to be the wrong call. The performance cost is real enough that many users developed a mindset that "async functions are slow" and started avoiding using it even in cases where the perf hit was affordable. Worse, we see nasty concurrency bugs where people think they can do some synchronous work at the top of a function and are dismayed to discover they've created race conditions. Overall, it seems users do not naturally assume an async function yields before executing any code.

所以,对于 Dart 2,我们现在正在采取非常痛苦的突破性更改将异步函数更改为与第一个 await 和通过该转换迁移我们所有现有的代码.我很高兴我们正在做出改变,但我真的希望我们做了正确的事情第一天.

So, for Dart 2, we are now taking the very painful breaking change to change async functions to be synchronous to the first await and migrating all of our existing code through that transition. I'm glad we're making the change, but I really wish we'd done the right thing on day one.

我不知道 Rust 的所有权和性能模型是否不同对你的限制,从顶部异步真的更好,但根据我们的经验,同步到第一个等待显然更好Dart 的权衡.

I don't know if Rust's ownership and performance model place different constraints on you where being async from the top really is better, but from our experience, sync-to-the-first-await is clearly the better trade-off for Dart.

cramert 回复(请注意,有些语法是现在已经过时了):

cramert replies (note that some of this syntax is outdated now):

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

If you need code to execute immediately when a function is called rather than later on when the future is polled, you can write your function like this:

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

代码示例

这些示例使用 Rust 1.39 中的异步支持和 futures crate 0.3.1.

Code examples

These examples use the async support in Rust 1.39 and the futures crate 0.3.1.

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);
    });
}

如果你调用 foo,Rust 中的事件顺序是:

If you called foo, the sequence of events in Rust would be:

  1. 返回了一些实现 Future 的东西.

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

That's it. No "actual" work is done yet. If you take the result of foo and drive it towards completion (by polling it, in this case via futures::executor::block_on), then the next steps are:

  1. 调用 long_running_operation 会返回一些实现 Future 的东西(它还没有开始工作).

  1. Something implementing Future<Output = u8> is returned from calling long_running_operation (it does not start work yet).

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

another_operation does work as it is synchronous.

.await 语法使long_running_operation 中的代码启动.foo 未来将继续返回未准备好",直到计算完成.

the .await syntax causes the code in long_running_operation to start. The foo future will continue to return "not ready" until the computation is done.

输出将是:

foo
another_operation
long_running_operation
Result: 3

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

Note that there are no thread pools here: this is all done on a single thread.

您也可以使用 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
}

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

Here we wrap synchronous code in an async block and then wait for both actions to complete before this function will be complete.

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

Note that wrapping synchronous code like this is not a good idea for anything that will actually take a long time; see What is the best approach to encapsulate blocking I/O in future-rs? for more info.

// 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
}

这篇关于Rust 中 async/await 的目的是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆