async/.await

第一章中我们简单领略了下async/await并用它们建立了一个简单的服务器。这一章会更深入地讨论async/.await的细节,解释它们如何工作,而async代码和传统的Rust程序又有怎样的不同。

async/.await是使让出当前线程的控制权而非阻塞成为可能的语法,让程序可以在等待某个行为完成的同时允许其他代码运行。

有两个使用async的主要方法:async fnasync块(block)。它们都会返回一个实现了Futuretrait的值:


// `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它。当对Future调用.await时,会试图将Future运行到完成状态。如果Future阻塞了,就让出当前线程的控制权。而可以继续执行时,Future就会被executor取出并回复运行,让.await得以解析(resolve)。

async 生命周期

与普通函数不同,获取引用或其他非'static参数的async fn会返回一个受参数声明周期约束的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必须 在其非'static参数还有效时 被.await。一般情况下,在调用函数后立即.await其返回的future(比如foo(&x).await)没有问题。但是如果 保存了此future 或 将其发送给了另一个task或线程,就可能出问题。

一个将含引用参数的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块中,我们延长了它的生命周期,使得它匹配了调用call时返回的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);
    }
}

多线程Executor的.await

需要注意的是,当使用一个多线程Futureexecutor时,一个Future可能会在线程间转移,因此async代码体重的任何变量同样有在线程间转移的可能,因为.await有可能导致切换至新的线程。

也就是说,使用Rc&RefCell或者其他没有实现Sendtrait的类型,以及 没有实现Sync的类型 的引用,都是不安全的。

(另:如果不是在调用.await时使用还是可以的。)

类似地,不应该在.await过程中使用一个传统的,非future感知的锁(non-futures-aware lock),因为其可能导致线程池的锁定:一个task除去锁,进行await并向executor让出,让另一个task尝试除去锁,因而造成死锁。为了避免这个现象,请使用futures::lock中的Mutex,而不是std::sync