async/.await
在第一章中,我们对 async/.await 已有了一个简单的了解。
本章将更详尽的介绍 async/.await,解读它是如何工作的,
以及 async 代码与传统的 Rust 同步程序有何不同。
async/.await 是 Rust 语法的特殊部分,通过它可以在本身产生阻塞时,
让出当前线程的控制权,即在等待自身完成时,亦可允许其它代码运行。
有两种方法来使用 async:async 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 可以避免这种情况。