async/.await

第一章中,我们对 async/.await 已有了一个简单的了解。 本章将更详尽的介绍 async/.await,解读它是如何工作的, 以及 async 代码与传统的 Rust 同步程序有何不同。

async/.await 是 Rust 语法的特殊部分,通过它可以在本身产生阻塞时, 让出当前线程的控制权,即在等待自身完成时,亦可允许其它代码运行。

有两种方法来使用 asyncasync fn 函数和 async 代码块。 它们都会返回一个实现了 Future 特征的值。


// `foo()` returns a type that implements `Future<Output = u8>`.
// `foo().await` will result in a value of type `u8`.
async fn foo() -> u8 { 5 }

fn bar() -> impl Future<Output = u8> {
    // This `async` block results in a type that implements
    // `Future<Output = u8>`.
    async {
        let x: u8 = foo().await;
        x + 5
    }
}

正如我们在第一章中所见,async 的代码和其它 futures 是惰性的: 除非去调用它们,否则它们不会做任何事。而最常用的运行 Future 的方法就是使用 .await。当 Future 调用 .await 时,这将尝试去运行 Future 直至完成它。 当 Future 阻塞时,它将让出线程的控制权。而当 Future 再次就绪时, 执行器会恢复其运行权限,使 .await 推动它完成。

async 的生命周期

不同于传统函数,async fns 接收引用或其它非静态参数, 并返回一个受其参数的生命周期限制的 Future

// This function:
async fn foo(x: &u8) -> u8 { *x }

// Is equivalent to this function:
fn foo_expanded<'a>(x: &'a u8) -> impl Future<Output = u8> + 'a {
    async move { *x }
}

这意味着,async fn 返回的 future,必须在其非静态参数的生命周期内调用 .await! 通常在调用函数后立即对 future 执行 .await 时不会出现问题(比如 foo(&x).await)。然而,当这个 future 被存储起来或发送到其它任务或线程上时, 这可能会成为一个问题。

一种常见的解决办法是,将引用参数(references-as-arguments)和 async fn 调用一并放置在一个 async代码块中, 这将 async fn 和引参转化成了一个 'static future。

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

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

通过将参数移动到 async 代码块中,我们将它的生命周期延长到同返回的 Future 一样久。

async move

同普通的闭包一样,async 代码块和闭包中可使用 move 关键字。 async move 代码块将获取其引用变量的所有权,使它得到更长的生命周期, 但这样做就不能再与其它代码共享这些变量了:

/// `async` block:
///
/// Multiple different `async` blocks can access the same local variable
/// so long as they're executed within the variable's scope
async fn blocks() {
    let my_string = "foo".to_string();

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

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

    // Run both futures to completion, printing "foo" twice:
    let ((), ()) = futures::join!(future_one, future_two);
}

/// `async move` block:
///
/// Only one `async move` block can access the same captured variable, since
/// captures are moved into the `Future` generated by the `async move` block.
/// However, this allows the `Future` to outlive the original scope of the
/// variable:
fn move_block() -> impl Future<Output = ()> {
    let my_string = "foo".to_string();
    async move {
        // ...
        println!("{my_string}");
    }
}

在多线程执行器上的 .await

注意,当使用多线程 Future 执行器时,Future 可能会在线程间移动, 所以在 async 里使用的任何变量都必须能在线程之间传输, 因为任何 .await 都可能导致任务切换到一个新线程上。

这意味着使用 Rc, &RefCell 或其它任何未实现 Send 特征的类型及未实现 Sync 特征的类型的引用都是不安全的。

(警告:只要在它们不在调用 .await 的代码块里就能使用这些类型。)

同样,在 .await 中使用传统的“非 future 感知”锁也并不是一个好主意, 它可能导致线程池死锁:一个任务在 .await 时获得了锁,然后交出运行权, 而执行器调度另一个任务同样想获取这个锁,这就导致了死锁。在 futures::lock 中 使用 Mutex 而不是 std::sync 可以避免这种情况。