博客主要是参考 Asynchronous Programming in Rust ,会结合简单的例子,对 async 和 await 做比较系统的理解,如何使用 async 和 await 是本节的重点。
async 和 await 主要用来写异步代码,async 声明的代码块实现了 Future 特性,如果实现 Future 的代码发生阻塞,会让出当前线程的控制权,允许线程去执行别的 Future 代码。
我把 async 代码块使用 Future 来表示,关键是理解 future 和执行线程之间的关系。系统中存在多个 future时,多个 future 的执行顺序如何控制?以及多个 future 如何同时并发执行?文章会尝试解释这些问题。
多个 async 串行执行
在 Cargo.toml 中增加如下依赖。
[dependencies]
futures = "0.3.28"
使用 async fn 创建异步方法,关键字 async 还可以用来声明代码块,在闭包函数中可能会见到。
async fn do_something() { /* ... */ }
我对原始内容的例子做了简化,来探究异步执行的过程。仍然声明了三个 async 方法,分别对应「学习唱歌」,「唱歌」,「跳舞」三个函数,假设这三个函数可以并行异步执行,那么这三个过程的顺序便是随机的、不确定的。
use futures::executor::block_on;
async fn learn_song() {
println!("learn song");
}
async fn sing_song() {
println!("sing song");
}
async fn dance() {
println!("dance");
}
async fn async_main() {
let f1 = learn_song();
let f2 = sing_song();
let f3 = dance();
futures::join!(f1, f2, f3);
}
fn main() {
block_on(async_main());
}
本地执行的结果是,上述代码无论执行多少次,输出都是确定的。理论上确实不应该,我都有点怀疑:会不会代码太简单导致的。
修改一下 learn_song 函数,在打印之前执行多次 for 循环。如果这几个过程是并发的,那么 learn_song 肯定是最后被执行完成的。遗憾的是,控制台第一个输出的还是 “learn song”。这也说明,上述代码并没有被并发执行。
async fn learn_song() {
let mut i = 0;
while i < 10000 {
i = i + 1;
}
println!("learn song");
}
原文代码示例
问题究竟出在哪里了呢?重新看一下原文章的demo,试着对比一下哪里出了问题。
async fn learn_and_sing() {
// Wait until the song has been learned before singing it.
// We use `.await` here rather than `block_on` to prevent blocking the
// thread, which makes it possible to `dance` at the same time.
let song = learn_song().await;
sing_song(song).await;
}
async fn async_main() {
let f1 = learn_and_sing();
let f2 = dance();
// `join!` is like `.await` but can wait for multiple futures concurrently.
// If we're temporarily blocked in the `learn_and_sing` future, the `dance`
// future will take over the current thread. If `dance` becomes blocked,
// `learn_and_sing` can take back over. If both futures are blocked, then
// `async_main` is blocked and will yield to the executor.
futures::join!(f1, f2);
}
fn main() {
block_on(async_main());
}
join!
和await
类似,只是join!
能够等待多个并发执行的 future,如果代码被临时阻塞在learn_and_sing
,dance
会被当前线程接管。如果dance
被阻塞,learn_and_sing
会重新被接管。如果两个方法同时被阻塞,async_main
就会被阻塞,会使当前执行阻塞。
通过代码注释,我抠到一个字眼: the current thread,这难道说明函数 f1 和 f2 是在一个线程上执行的?如果它们是在一个线程上执行的,又因为方法体内部都没有类似网络IO的阻塞,那确实有可能导致串行执行的效果。
rust 对 async 的支持
重新翻看语言和库的支持 Language and library support介绍,我决定把中心放在 async 的设计实现上,async 在底层是如何被处理的。
- 最基础的特性、类型和方法,比如 Future 特性都被标准库提供
- async/await 语法直接被 Rust 编译器支持
- futures 库提供了一些工具类型、宏、函数,它们可以被使用在 Rust 应用程序中
async runtimes
比如 Takio、async-std,提供了执行一部代码、IO、创建任务的能力。大部分的异步应用和异步库都依赖具体的运行时,可以查看 The Async Ecosystem 了解细节
关键点在异步运行时上,原来 rust 只提供了一套异步运行时的接入标准,具体的实现则是由第三方库自己决定的,比较有名的的运行时三方库包括 Takio 和 async-std。不得不说,Rust 设计的眼界确实比较高。
我想通过 Single Threaded vs Multi-Threaded Executors 来理解单线程和多线程的执行。前面的例子中,我们有怀疑:代码没有并行是因为在单线程中执行导致的。当然,也可能是因为我们没有明确指定 async runtimes。
异步的 executor 可以是单线程、也可以是多线程执行,async-executor 库就同时提供了这两种 executor
.
多线程 executor 可以同时处理多个任务,这会大大加快了处理速度。但任务之间的数据同步成本也会更高一些。建议通过性能测试来决策选择单线程还是多线程
.
任务既可以被创建它的线程执行,也可以被其他独立线程执行。即使任务被独立线程执行,其运行也是非阻塞的。如果想要调度任务在多线程上执行,任务本身必须要实现 Send 特性。有的运行时还支持创建 non-Send 任务,用来保证每个任务都被创建它的线程执行。有的库中还支持将生成的阻塞任务交给特定的线程去执行,这对于运行阻塞代码会非常有用
看到这里,我决定指定 Takio 运行时来重新运行上述代码,如何指定呢?