Rust Async 异步编程(五):async/.await

Rust Async 异步编程(五):async/.await

  • [Rust Async 异步编程(五):async/.await](#Rust Async 异步编程(五):async/.await)
    • [使用 async 的两种方法](#使用 async 的两种方法)
    • [.await 做了什么?](#.await 做了什么?)
    • [async 的生命周期](#async 的生命周期)
    • [async move](#async move)
    • [在多线程执行器上进行 .await](#在多线程执行器上进行 .await)
    • 参考

Rust Async 异步编程(五):async/.await

async/.await 是 Rust 语法的一部分,它在遇到阻塞操作时(例如 I/O)会让出当前线程的所有权而不是阻塞当前线程,这样就允许当前线程继续去执行其它代码,最终实现并发。

使用 async 的两种方法

有两种方式可以使用 async:

  • async fn用于声明函数
  • async { ... } 用于声明语句块

它们会返回一个实现 Future trait 的值。

rust 复制代码
// `foo()`返回一个`Future<Output = u8>`,
// 当调用`foo().await`时,该`Future`将被运行,当调用结束后我们将获取到一个`u8`值
async fn foo() -> u8 { 5 }

fn bar() -> impl Future<Output = u8> {
    // 下面的`async`语句块返回`Future<Output = u8>`
    async {
        let x: u8 = foo().await;
        x + 5
    }
}

无需调整 async fn 的返回类型,Rust 自动把它当成相应的 Future 类型。返回 Future 包含所需相关信息:参数、本地变量空间...

Future 的具体类型由编译器基于函数体和参数自动生成:

  • 该类型没有名称
  • 它实现了 Future<Output=R>

async 是懒惰的,直到被执行器 poll 或者 .await 后才会开始运行,其中后者是最常用的运行 Future 的方法。 当 .await 被调用时,它会尝试运行 Future 直到完成,但是若该 Future 进入阻塞,那就会让出当前线程的控制权。当 Future 后面准备再一次被运行时(例如从 socket 中读取到了数据),执行器会得到通知,并再次运行该 Future ,如此循环,直到由 .awiat 完成解析。

这种途中能暂停执行,然后恢复执行的能力是 async 独有的。由于 await 表达式依赖于"可恢复执行"这个特性,所以 await 只能用在 async 里。

.await 做了什么?

  1. 获得 Future 的所有权,对其执行 poll
  2. 如果 Future Ready,其最终值就是 await 表达式的值,这时执行就可以继续了
  3. 否则就返回 Pending 给调用者

async 的生命周期

与传统函数不同,async fn 函数如果拥有引用或其他非 'static 类型的参数,那它返回的 Future 的生命周期就会绑定到参数的生命周期上,被这些参数的生命周期所限制。

这意味着 async fn 返回的 Future,在 .await 的同时,函数的引用或其他非 'static 类型的参数必须保持有效。

rust 复制代码
async fn foo(x: &u8) -> u8 { *x }

上面的函数跟下面的函数是等价的:

rust 复制代码
fn foo_expanded<'a>(x: &'a u8)
-> impl Future<Output = u8> + 'a {
    async move { *x }
}

意味着 async fn 函数返回的 Future 必须满足以下条件:当 x 依然有效时, 该 Future 就必须继续等待(.await),也就是说 x 必须比 Future 活得更久。

在一般情况下,在函数调用后就立即 .await 不会存在任何问题,例如 foo(&x).await。

但是,若 Future 被先存起来或发送到另一个任务或者线程,就可能存在问题了:

rust 复制代码
use std::future::Future;

fn bad() -> impl Future<Output = u8> {
    let x = 5;
    borrow_x(&x) // ERROR: `x` does not live long enough
}

async fn borrow_x(x: &u8) -> u8 { *x }

以上代码会报错,因为 x 的生命周期只到 bad 函数的结尾。 但是 Future 显然会活得更久:

复制代码
error[E0597]: `x` does not live long enough
 --> src/main.rs:4:14
  |
4 |     borrow_x(&x) // ERROR: `x` does not live long enough
  |     ---------^^-
  |     |        |
  |     |        borrowed value does not live long enough
  |     argument requires that `x` is borrowed for `'static`
5 | }
  | - `x` dropped here while still borrowed

其中一个常用的解决方法就是将具有引用参数的 async fn 函数转变成一个具有 'static 生命周期的 Future。以上解决方法可以通过将参数和对 async fn 的调用放在同一个 async 语句块来实现:

rust 复制代码
use std::future::Future;

async fn borrow_x(x: &u8) -> u8 { *x }

fn good() -> impl Future<Output = u8> {
    async {
        let x = 5;
        borrow_x(&x).await
    }
}

如上所示,通过将参数移动到 async 语句块内, 我们将它的生命周期扩展到 'static, 并跟返回的 Future 保持了一致。

async move

async 允许我们使用 move 关键字来将环境中变量的所有权转移到语句块内,就像闭包那样,好处是你不再发愁该如何解决借用生命周期的问题,坏处就是无法跟其它代码实现对变量的共享:

rust 复制代码
// 多个不同的 `async` 语句块可以访问同一个本地变量,只要它们在该变量的作用域内执行
async fn blocks() {
    let my_string = "foo".to_string();

    let future_one = async {
        // ...
        println!("{my_string}");
    };

    let future_two = async {
        // ...
        println!("{my_string}");
    };

    // 运行两个 Future 直到完成
    let ((), ()) = futures::join!(future_one, future_two);
}



// 由于`async move`会捕获环境中的变量,因此只有一个`async move`语句块可以访问该变量,
// 但是它也有非常明显的好处: 变量可以转移到返回的 Future 中,不再受借用生命周期的限制
fn move_block() -> impl Future<Output = ()> {
    let my_string = "foo".to_string();
    async move {
        // ...
        println!("{my_string}");
    }
}

在多线程执行器上进行 .await

需要注意的是,当使用多线程 Future 执行器(executor)时, Future 可能会在线程间被移动,因此 async 语句块中的变量必须要能在线程间传递。 至于 Future 会在线程间移动的原因是:它内部的任何 .await 都可能导致它被切换到一个新线程上去执行。

由于需要在多线程环境使用,意味着 Rc、RefCell、没有实现 Send trait 的所有权类型、没有实现 Sync trait 的引用类型,它们都是不安全的,因此无法被使用。

需要注意,实际上它们还是有可能被使用的,只要在 .await 调用期间,它们没有在作用域范围内。

类似的原因,在 .await 时使用普通的、对 Future 无感知的锁也不安全,例如 Mutex。原因是,它可能会导致线程池被锁:当一个任务获取锁 A 后,若它将线程的控制权还给执行器,然后执行器又调度运行另一个任务,该任务也去尝试获取了锁 A ,结果当前线程会直接卡死,最终陷入死锁中。

因此,为了避免这种情况的发生,我们需要使用 futures 包下的锁 futures:🔒:Mutex 来替代 std::sync::Mutex 完成任务。

参考

  1. https://github.com/rustcn-org/async-book
  2. https://www.bilibili.com/video/BV1Ki4y1C7gj