async/.await

第一章,我们简单介绍了 async/.await,并且用它构建一个简单的服务器。这一章会详细讨论 async/.await,解释它如何工作以及 async 代码如何和传统Rust程序不同。

async/.await 是特殊的Rust语法,使得让出当前线程控制权成为可能,而不是阻塞它,也允许其他代码在等待一个操作完成时取得进展。

有两种主要的方法使用 async: async fnasync 块。两种方法都返回一个实现了 Future trait 的值:


// `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 体以及其他 future 类型是惰性的:除非它们运行起来,否则它们什么都不做。运行 Future 最常见的方法是 .await 它。当 .awaitFuture 上调用时,它会尝试把 future 跑到完成状态。如果 Future 被阻塞了,它会让出当前线程的控制权。能取得进展时,执行器就会捡起这个 Future 并继续执行,让 .await 求解。

async 生命周期

和传统函数不同,async fn 会获取引用以及其他拥有非 'static 生命周期的参数,并返回被这些参数的 生命周期约束的 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 }
}

这意味着这些 future 被 async fn 函数返回后必须要在它的非 'static 参数仍然有效时 .await。 在通常的场景中,future 在函数调用后马上 .await(例如 foo(&x).await),并不会有大问题。然而,如果储存了这些 future 或者把它发送到其他的任务或者线程,那就有问题了。

把带有引用参数的 async fn 转化成一个'staticfuture 的一个常用的规避方法是把这些参数 和对 async fn 的函数调用封装到async 块中:

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 块中,我们把它的生命周期扩展到了匹配调用 foo 函数返回的 Future 的生命周期。

async move

async 块和闭包允许使用 move 关键字,这和普通的闭包一样。一个 async move 块会获取 所指向变量的所有权,允许它的生命周期超过当前作用域(outlive),但是放弃了与其他代码共享这些变量的能力:

/// `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 trait 的类型是不安全的,包括那些指向 没有 Sync trait 类型的引用。

(注意:使用这些类型是允许的,只要他们不是在调用 .await 的作用域内。)

类似的,横跨 .await 持有一个非 future 感知的锁这种做法是很不好的,因为它能导致整个线程池 锁上:一个任务可能获得了锁,.await 然后让出到执行器,允许其他任务尝试获取所并导致死锁。 为了避免这种情况,使用 futures::lock里的 Mutex 类型比起 std::sync 里面的更好。