Rust 异步编程和 tokio 的基础知识的回顾:
对 Rust 异步并发相关的知识点做补遗。
Rust 中的 async 函数,和 Rust 的普通函数不相容。
async Rust 是 Rust 里的独立王国。
Rust async 具有传染性。用 async/.await 的两条规则。
代码(非 tokio 那个顶层 Runtime 代码)调用 async 函数,调用者函数也要是 async 的:
//
// 我们定义foo1为一个异步函数
async fn foo1() -> u32 {
100u32
}
// 我需要在foo2函数中调用foo1,那么这个foo2也必需要是async函数
async fn foo2() -> u32 {
foo1().await
}
// 我需要在foo3函数中调用foo2,那么这个foo3也必需要是async函数
async fn foo3() -> u32 {
foo2().await
}
#[tokio::main]
async main() {
let num = foo3().await;
println!("{}", num);
}
async 代码的传染性,由 Rust 的 async 语法带来的。
注:Rust 中还有一个语法具有传染性——类型参数 T。
异步代码与同步代码混合使用,怎么处理?
同步代码分两类:
一类是直接在内存里的简单操作,如 vec.push() 这种 API 接口的调用。
这类,在 std Rust 里怎么使用,在 async Rust 里就怎么使用,一样的:
//
async fn foo1() -> u32 {
let mut vec = Vec::new();
vec.push(10);
}
另一类,要执行长时间的计算,或调用第三方的同步库的代码,没法去修改它。
这类函数,也可以直接调用:
//
async fn foo1() -> u32 {
let result = a_heavy_work();
}
可运行,但有性能问题:会阻塞当前正在跑这个异步代码的系统线程(OS Thread,由 tokio 来管理维护),当前的这个系统线程就会被卡住,不能再跑其他异步代码了。
tokio 专门提供了 task::spawn_blocking() 函数:
//
#[tokio::main]
async fn main() {
// 此任务跑在一个单独的线程中
let blocking_task = tokio::task::spawn_blocking(|| {
// 在这里面可以执行阻塞线程的代码
});
// 像下面这样用同样的方式等待这种阻塞式任务的完成
blocking_task.await.unwrap();
}
要把 CPU 计算密集型任务放到 task::spawn_blocking() 里,tokio 会单独开一个新的系统线程,专门跑这个 CPU 计算密集型的 task。和普通的 tokio task 一样,通过 await 来获取结果,也可以用 Oneshot channel 把结果返回回来。
给计算密集型任务单独开一个系统线程,防止异步并发能力下降 --> 比较好的方案。
当主体是 async 代码,小部分是同步代码,用 task::spawn_blocking() 较合适。
若主体代码是同步代码(或 std Rust 代码),局部调用 async 接口,如 db driver 只提供了 async 封装,怎么办?
展开 #[tokio::main]。
//
#[tokio::main]
async fn main() {
println!("Hello world");
}
// 展开后,其实是下面这个样子:
fn main() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
println!("Hello world");
})
}
要在同步代码中执行 async 代码,只要手动 block_on 这段异步代码就可以了。
除了默认的系统多线程 Runtime 外,tokio 专门为临时(及测试)场景提供了另一种单系统线程的 runtime,即 new_current_thread()。在当前程序执行的线程中建立 tokio Runtime,异步任务就跑在当前这个线程中。如:
//
async fn foo1() -> u32 {
10
}
fn foo() {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build().unwrap();
let num = rt.block_on(foo1()); // 注意这一句的 foo1(),调用了此异步函数
// 或者像下面这样写
//let num = rt.block_on(async {
// foo1().await
//});
println!("{}", num);
}
fn main() {
foo();
}
// 输出
10
在主体为 std Rust 的代码中,成功地调用了局部的 async Rust 代码,得到局部异步代码的返回值。
Rust 的 async 是一种无栈协程(Stackless Coroutine)方案。非常高效,性能在所有支持异步语法的语言中属于最高的那一级。
Rust 把 async 语法编译成 std Rust 中的状态机,通过运行时底层的 poll 机制来轮询这个状态机的状态。
本质上,async/.await 只是语法糖。
简单来说,Rust 会把一个 async 函数转换成另一种东西,你可以看一下我给出的转换示例。async 函数:
//
async fn foo1() -> u32 {
10
}
// 转换后:
struct FutureA {
...
}
impl Future for FutureA {
...
}
Rust 的实现不像 Go 或 Java (在系统级线程基础上,单独实现了一层结合 GC 内存管理且具有完整屏蔽性的轻量级线程),没有选择在 OS 应用之间引入一个层(layer),是在结构体之上构建一个状态机,以零成本抽象(zero cost abstract)为原则,尽量少地引入额外的消耗,配合 async/.await 语法糖,来简化开发。
Rust 的异步并发能力达到业界顶尖。世界开源协作的典范。
Rust async 一直不断地发展,如目前在 trait 里,不能定义 async 方法:
//
trait MyTrait {
async fn f() {}
}
// 编译错误
error[E0706]: trait fns cannot be declared `async`
--> src/main.rs:4:5
|
4 | async fn f() {}
|
为解决这个问题,可引入 async_trait crate 的 async_trait 宏来暂时过渡。
//
use async_trait::async_trait;
#[async_trait] // 定义时加
trait MyTrait {
async fn f() {}
}
struct Modal;
#[async_trait] // impl 时也要加
impl MyTrait for Modal {}
定义 trait 和 impl trait 时,都要加 #[async_trait] 属性宏来标注。之后,trait 里的 async fn 就可以像普通的 async fn 那样在异步代码中被调用了。
目前使用这个宏会有一点性能上的开销,估计 1.75 版本之后,就可以去掉这个宏标注了。
补充了 Rust 异步并发编程中要注意的知识点。
5 课讲 async Rust 和 tokio ,异步并发编程对 高性能高并发服务 至关重要。
后面 Web 后端服务开发实战,继续 tokio 讲解。
如何理解 “async Rust 是一个独立王国”这种说法?
参考链接:https://tokio.rs/tokio/topics/bridging