繁体   English   中英

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

[英]What is the purpose of async/await in Rust?

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

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

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

    result = task.Result;
}

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

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

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

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

你混淆了几个概念。

并发不是并行,而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)
}

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

注意“之前”的代码基本就是今天的poll_fn函数的实现

另请参阅Peter Hall关于如何更好地跟踪许多变量的回答

参考

关于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
}

这有效!

调用async函数不会运行任何东西

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

为什么Future不会在创建时立即轮询?

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

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

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

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

来自 munificent 的讨论涵盖了 Dart 2.0 的一些背景:

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

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

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

  2. 总是让步意味着某些模式不能使用 async/await 实现。 特别是,像这样的代码真的很常见(这里是伪代码):

     getThingFromNetwork(): if (downloadAlreadyInProgress): return cachedFuture cachedFuture = startDownload() return cachedFuture

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

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

我们为我们的案子辩护,但最终语言设计者坚持使用自上而下的异步方式。 这是几年前的事了。

结果证明这是错误的调用。 性能成本是实实在在的,以至于许多用户养成了“异步函数很慢”的心态,并开始避免使用它,即使在性能下降的情况下也是如此。 更糟糕的是,我们看到令人讨厌的并发错误,人们认为他们可以在函数的顶部做一些同步工作,但发现他们创造了竞争条件而感到沮丧。 总体而言,用户似乎不会在执行任何代码之前自然地假设异步函数会产生收益。

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

我不知道 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()) } }

代码示例

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

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

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

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

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

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

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

  3. .await语法会导致代码long_running_operation启动。 foo未来将继续返回“未准备好”,直到计算完成。

输出将是:

foo
another_operation
long_running_operation
Result: 3

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

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
}

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

请注意,像这样包装同步代码对于实际上需要很长时间的任何事情都不是一个好主意; 请参阅在 future-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
}

考虑这个简单的伪 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));

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

它引入了许多一次性变量,可以说比带有 promise 的原始版本更糟糕。 那么何必呢?

考虑这种变化,稍后在计算中需要变量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);
}

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

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 / await版本不会遇到这个问题。

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

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

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


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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM