Rust Async 异步编程(五):async/.await
Rust Async 异步编程(五):async/.await
- Rust Async 异步编程(五):async/.await
- 使用 async 的两种方法
- .await 做了什么?
- async 的生命周期
- async move
- 在多线程执行器上进行 .await
- 参考
Rust Async 异步编程(五):async/.await
async/.await 是 Rust 语法的一部分,它在遇到阻塞操作时(例如 I/O)会让出当前线程的所有权而不是阻塞当前线程,这样就允许当前线程继续去执行其它代码,最终实现并发。
使用 async 的两种方法
有两种方式可以使用 async:
- async fn用于声明函数
- async { … } 用于声明语句块
它们会返回一个实现 Future trait 的值。
// `foo()`返回一个`Future<Output = u8>`,
// 当调用`foo().await`时,该`Future`将被运行,当调用结束后我们将获取到一个`u8`值
async fn foo() -> u8 { 5 }fn bar() -> impl Future<Output = u8> {// 下面的`async`语句块返回`Future<Output = u8>`async {let x: u8 = foo().await;x + 5}
}
无需调整 async fn 的返回类型,Rust 自动把它当成相应的 Future 类型。返回 Future 包含所需相关信息:参数、本地变量空间…
Future 的具体类型由编译器基于函数体和参数自动生成:
- 该类型没有名称
- 它实现了 Future<Output=R>
async 是懒惰的,直到被执行器 poll 或者 .await 后才会开始运行,其中后者是最常用的运行 Future 的方法。 当 .await 被调用时,它会尝试运行 Future 直到完成,但是若该 Future 进入阻塞,那就会让出当前线程的控制权。当 Future 后面准备再一次被运行时(例如从 socket 中读取到了数据),执行器会得到通知,并再次运行该 Future ,如此循环,直到由 .awiat 完成解析。
这种途中能暂停执行,然后恢复执行的能力是 async 独有的。由于 await 表达式依赖于“可恢复执行”这个特性,所以 await 只能用在 async 里。
.await 做了什么?
- 获得 Future 的所有权,对其执行 poll
- 如果 Future Ready,其最终值就是 await 表达式的值,这时执行就可以继续了
- 否则就返回 Pending 给调用者
async 的生命周期
与传统函数不同,async fn 函数如果拥有引用或其他非 'static 类型的参数,那它返回的 Future 的生命周期就会绑定到参数的生命周期上,被这些参数的生命周期所限制。
这意味着 async fn 返回的 Future,在 .await 的同时,函数的引用或其他非 'static 类型的参数必须保持有效。
async fn foo(x: &u8) -> u8 { *x }
上面的函数跟下面的函数是等价的:
fn foo_expanded<'a>(x: &'a u8)
-> impl Future<Output = u8> + 'a {async move { *x }
}
意味着 async fn 函数返回的 Future 必须满足以下条件:当 x 依然有效时, 该 Future 就必须继续等待(.await),也就是说 x 必须比 Future 活得更久。
在一般情况下,在函数调用后就立即 .await 不会存在任何问题,例如 foo(&x).await。
但是,若 Future 被先存起来或发送到另一个任务或者线程,就可能存在问题了:
use std::future::Future;fn bad() -> impl Future<Output = u8> {let x = 5;borrow_x(&x) // ERROR: `x` does not live long enough
}async fn borrow_x(x: &u8) -> u8 { *x }
以上代码会报错,因为 x 的生命周期只到 bad 函数的结尾。 但是 Future 显然会活得更久:
error[E0597]: `x` does not live long enough--> src/main.rs:4:14|
4 | borrow_x(&x) // ERROR: `x` does not live long enough| ---------^^-| | || | borrowed value does not live long enough| argument requires that `x` is borrowed for `'static`
5 | }| - `x` dropped here while still borrowed
其中一个常用的解决方法就是将具有引用参数的 async fn 函数转变成一个具有 'static 生命周期的 Future。以上解决方法可以通过将参数和对 async fn 的调用放在同一个 async 语句块来实现:
use std::future::Future;async fn borrow_x(x: &u8) -> u8 { *x }fn good() -> impl Future<Output = u8> {async {let x = 5;borrow_x(&x).await}
}
如上所示,通过将参数移动到 async 语句块内, 我们将它的生命周期扩展到 'static, 并跟返回的 Future 保持了一致。
async move
async 允许我们使用 move 关键字来将环境中变量的所有权转移到语句块内,就像闭包那样,好处是你不再发愁该如何解决借用生命周期的问题,坏处就是无法跟其它代码实现对变量的共享:
// 多个不同的 `async` 语句块可以访问同一个本地变量,只要它们在该变量的作用域内执行
async fn blocks() {let my_string = "foo".to_string();let future_one = async {// ...println!("{my_string}");};let future_two = async {// ...println!("{my_string}");};// 运行两个 Future 直到完成let ((), ()) = futures::join!(future_one, future_two);
}// 由于`async move`会捕获环境中的变量,因此只有一个`async move`语句块可以访问该变量,
// 但是它也有非常明显的好处: 变量可以转移到返回的 Future 中,不再受借用生命周期的限制
fn move_block() -> impl Future<Output = ()> {let my_string = "foo".to_string();async move {// ...println!("{my_string}");}
}
在多线程执行器上进行 .await
需要注意的是,当使用多线程 Future 执行器(executor)时, Future 可能会在线程间被移动,因此 async 语句块中的变量必须要能在线程间传递。 至于 Future 会在线程间移动的原因是:它内部的任何 .await 都可能导致它被切换到一个新线程上去执行。
由于需要在多线程环境使用,意味着 Rc、RefCell、没有实现 Send trait 的所有权类型、没有实现 Sync trait 的引用类型,它们都是不安全的,因此无法被使用。
需要注意,实际上它们还是有可能被使用的,只要在 .await 调用期间,它们没有在作用域范围内。
类似的原因,在 .await 时使用普通的、对 Future 无感知的锁也不安全,例如 Mutex。原因是,它可能会导致线程池被锁:当一个任务获取锁 A 后,若它将线程的控制权还给执行器,然后执行器又调度运行另一个任务,该任务也去尝试获取了锁 A ,结果当前线程会直接卡死,最终陷入死锁中。
因此,为了避免这种情况的发生,我们需要使用 futures 包下的锁 futures:🔒:Mutex 来替代 std::sync::Mutex 完成任务。
参考
- https://github.com/rustcn-org/async-book
- https://www.bilibili.com/video/BV1Ki4y1C7gj