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

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

问题描述

使用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;
}

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

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中,我读了

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

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.

在这种情况下,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
}

请注意,之前"代码基本上是

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

另请参阅彼得·霍尔的答案有关如何更好地跟踪许多变量的信息.

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.

有关最权威的答案,请查看从无船而来的评论关于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.

与此有关的一点是,异步&在Rust中等待并不是天生的 并发构造.如果您的程序仅使用异步& 等待并且没有并发原语,程序中的代码将 以定义的,静态已知的线性顺序执行.显然,大多数 程序将使用某种并发性来安排多个时间, 事件循环上的并发任务,但不必如此.这是什么 意味着您可以-琐碎-本地保证 某些事件,即使在两者之间执行无阻塞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成为现实的低级控件.跑起来 到第一个等待点不会固有地违反该规定-您会 仍然知道执行代码的时间,它将只执行两次 不同的地方,取决于它是在一个之前还是之后 等待.但是,我认为其他语言的决定开始了 立即执行很大程度上源于他们的系统 调用异步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).

此讨论内容来自Dunk 2.0. :

我在Dart团队中. Dart的async/await主要由 Erik Meijer,他还从事异步/等待C#的工作.在C#中,异步/等待 与第一次等待同步.对于Dart,Erik和其他人感到 C#的模型过于混乱,因此指定了异步 函数在执行任何代码之前总是产生一次.

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/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,我们现在将非常痛苦的突破性变化 将异步功能更改为与第一次等待同步,并 在此过渡过程中迁移所有现有代码.我很高兴 我们正在做出改变,但我真的希望我们做对了 在第一天.

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中的异步支持,以及期货板条箱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<Output = u8>的内容.
  1. Something implementing Future<Output = u8> is returned.

就是这样.尚未完成任何实际"工作.如果采用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<Output = u8>的内容(尚未开始工作).

  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中异步/等待的目的是什么?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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