async
/.await
在第一章,我们简单介绍了 async
/.await
,并且用它构建一个简单的服务器。这一章会详细讨论 async
/.await
,解释它如何工作以及 async
代码如何和传统Rust程序不同。
async
/.await
是特殊的Rust语法,使得让出当前线程控制权成为可能,而不是阻塞它,也允许其他代码在等待一个操作完成时取得进展。
有两种主要的方法使用 async
: async fn
和 async
块。两种方法都返回一个实现了 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
它。当 .await
在 Future
上调用时,它会尝试把 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
转化成一个'static
future 的一个常用的规避方法是把这些参数 和对 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
里面的更好。