Rust 语言从入门到实战 唐刚 学习笔记17

进阶篇 (6讲)

17|tokio编程: Rust异步编程还有哪些需要注意的点?

 Rust 异步编程和 tokio 的基础知识的回顾:

  1. async/.await 语法
  2. tokio 基本概念和组件
  3. 用 tokio 编写网络并发应用
  4. 用 Arc 和 Mutex 在多个 task 之间共享数据
  5. 在并发 task 之间使用 Channel 传递数据

对 Rust 异步并发相关的知识点做补遗。

async 其他知识点

Rust 中的 async 函数,和 Rust 的普通函数不相容。

async Rust 是 Rust 里的独立王国。

async 代码的传染性

Rust async 具有传染性。用 async/.await 的两条规则。

  1. async 函数或 block 只有被 .await 后才被驱动。
  2. .await 只能在 async 函数或 block 中使用。

代码(非 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。

异步代码与同步代码混合使用,怎么处理?

async 代码环境中的同步代码

同步代码分两类:

一类是直接在内存里的简单操作,如 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() 较合适。

同步代码环境中的 async 代码

若主体代码是同步代码(或 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 实现原理

Rust 的 async 是一种无栈协程(Stackless Coroutine)方案。非常高效,性能在所有支持异步语法的语言中属于最高的那一级。

Rust async 的背后原理

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 讲解。

Rust 语言从入门到实战 唐刚 学习笔记17_第1张图片

思考题

如何理解 “async Rust 是一个独立王国”这种说法?

参考链接:https://tokio.rs/tokio/topics/bridging

你可能感兴趣的:(Rust,语言从入门到实战,唐刚,学习笔记,rust,学习,笔记)