代码之家  ›  专栏  ›  技术社区  ›  Boiethios

Rust中async/await的用途是什么?

  •  6
  • Boiethios  · 技术社区  · 7 年前

    在C#这样的语言中,给出以下代码(我没有使用 await

    async Task Foo()
    {
        var task = LongRunningOperationAsync();
    
        // Some other non-related operation
        AnotherOperation();
    
        result = task.Result;
    }
    

    Task 返回(那是一个未来)。然后可以执行与第一个操作并行运行的另一个操作,最后可以等待操作完成。我认为这也是 async / 等待

    另一方面,在《铁锈》中,我读到了 the RFC 那就是:

    Rust's futures和其他语言的futures之间的一个根本区别是,除非接受民意调查,否则Rust's futures什么也做不了。整个系统都是围绕着这一点构建的:例如,取消正是因为这个原因而放弃了未来。相反,在其他语言中,调用异步fn会带来一个立即开始执行的未来。

    在这种情况下,我们的目的是什么 异步 生锈了?与其他语言相比,这种表示法是运行并行操作的一种方便方法,但是如果调用 函数不运行任何操作。

    3 回复  |  直到 7 年前
        1
  •  37
  •   Shepmaster Tim Diekmann    6 年前

    你把几个概念混为一谈了。

    Concurrency is not parallelism ,和 async await 是用来 ,这有时可能意味着它们也是并行的工具。

    此外,是否立即轮询future与所选语法是正交的。

    异步 /

    关键词 异步 使创建和交互异步代码更容易阅读,看起来更像“普通”同步代码。据我所知,在所有有这些关键字的语言中都是这样。

    更简单的代码

    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”代码基本上是 implementation of today's poll_fn function

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

    工具书类

    可能令人惊讶的事情之一 异步 / 等待 它实现了一种以前不可能实现的特定模式:在未来使用引用。下面是一些以异步方式用值填充缓冲区的代码:

    之前

    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 异步 . 事实上,锈菌在灭绝之前有一个欣欣向荣的异步生态系统(比如东京) / 等待

    你为什么不呢 未来 在创建时是否立即进行了民意测验?

    要获得最权威的答案,请查看 this comment from withoutboats 在RFC请求中:

    语言是铁锈的未来不做任何事,除非民意调查。这个 整个系统都是围绕这一点构建的:例如,取消是 正是因为这个原因放弃了未来。相反,在其他方面 马上。

    关于这一点的一点是,异步;在铁锈中等待不是天生的 并行构造。如果您的程序只使用异步; 以定义的、静态已知的、线性顺序执行。显然,大多数 事件循环上的并发任务,但它们不必这样做。这是什么 意思是你可以在本地保证 某些事件,即使其间执行了非阻塞IO 你希望与一些更大的非局部变量集异步 请求处理程序,同时与许多其他请求并发 处理程序,甚至在等待点的两侧)。

    这个属性赋予Rust的async/await语法类似于本地的语法 推理和;低水平的控制,使生锈是什么。跑起来 到了第一个等待点就不会违反这一点-你会 仍然知道代码何时执行,它只会在两分钟内执行 不同的地方取决于它是在 等待。不过,我认为决定由其他语言开始 立即执行很大程度上源于他们的系统 (例如,这就是我对潜在问题的印象 来自Dart 2.0文档)。

    this discussion from munificent

    Erik Meijer,他也为C#开发了async/await。在C#中,异步/等待 C#的模型过于混乱,而是指定异步

    当时,我和我团队中的另一个人的任务是 包管理器。基于这种经验,我们感觉到了异步函数 应该同步运行到第一个等待。我们的论点是 主要是:

    1. 做。即使在你能忍受的情况下,流血也是一种阻力 到处都是小表演。

    2. 总是让步意味着某些模式不能使用async/await实现。尤其是,像这样的代码非常常见 (此处为伪代码):

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

      以前创建的挂起的未来。你要确保你不会 操作多次。这意味着你需要同步

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

    我们为自己辩护,但最终语言设计师还是坚持了下来

    结果是打错电话了。性能成本是真实的 以至于许多用户形成了“异步函数是 慢”并开始避免使用它,即使是在性能命中的情况下 价格实惠。更糟糕的是,我们看到了令人讨厌的并发错误 认为他们可以在函数的顶部做一些同步工作 当发现他们创造了比赛条件时感到沮丧。总的来说 执行任何代码。

    所以,对于飞镖2,我们现在要做的是非常痛苦的改变 将异步函数更改为与第一个wait和 在第一天。

    但根据我们的经验,同步到第一个等待显然是更好的 折衷飞镖。

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

    功能如下:

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

    这些示例使用Rust1.39和FuturesCrate0.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

    1. 实施的东西 Future<Output = u8> 已返回。

    并推动它完成(通过轮询,在本例中是通过 futures::executor::block_on

    1. 实施的东西 从呼叫返回 long_running_operation (尚未开始工作)。

    2. another_operation 因为它是同步的。

    3. 这个 .await 语法导致代码 开始。这个 在计算完成之前,future将继续返回“not ready”。

    输出为:

    foo
    another_operation
    long_running_operation
    Result: 3
    

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

    异步 阻碍

    阻碍:

    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
    }
    

    异步 阻塞,然后等待两个操作完成,此函数才能完成。

    请注意,像这样包装同步代码 对于任何需要很长时间的事情都是个好主意;看见 What is the best approach to encapsulate blocking I/O in future-rs? 更多信息。

    使用线程池

    // 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
    }
    
        2
  •  6
  •   Shepmaster Tim Diekmann    7 年前

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

    它引入了许多一次性变量,可以说比最初的有承诺的版本更糟糕。那何必费心呢?

    考虑这个变化,变量 response objects 在以后的计算中需要:

    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 await

        3
  •  4
  •   Jason Orendorff Oliver    5 年前

    目的 async / await 在Rust中,它提供了一个用于concurrencysame的工具包,如C#和其他语言。

    异步 方法立即开始运行,并且无论您是否 等待 不管结果如何。在Python和Rust中,当你调用 异步 方法,直到您 等待 是的。但无论哪种方式,基本上都是相同的编程风格。

    async_std::task::spawn tokio::task::spawn


    为什么? 铁锈 异步

    • 在C和JS中,每个 异步 方法调用被隐式添加到全局可变队列中。这是某种隐含语境的副作用。不管是好是坏,这不是Rust的风格。

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