likes
comments
collection
share

Rust 异步编程简单入门

作者站长头像
站长
· 阅读数 31

Rust 异步编程简单入门

再次祭出陈天老师的 Rust 学习路径图,记得当初初学 Rust 的时候异步编程就看了个大概,直接略过了底层原理的探究,尤其是自引用和Pin之类的概念相当枯燥难懂,理解看起来着实费劲。如果你的工作大部分是 CRUD,写一些和数据库打交道的 HTTP API,底层原理的东西的确用处不大,但如果你想让自己的技能树稍微丰富一些,能做一些别人做不了的事情,能面对不同的场景设计出来更高效的系统,那就需要系统的认真的学习 Rust 异步的设计思路和精髓。先用一个简单例子进入 Rust 异步编程的世界。

Rust 异步编程 async/.await

async/.await 是 Rust 内置的语言特性,可以让我们用同步的方式去编写异步的代码。

下面我们来通过例子学习 async/.await 关键字该如何使用,在开始之前,需要先引入 futures 包。编辑 Cargo.toml 文件并添加以下内容:

[dependencies]
futures = "0.3"

使用 async 创建一个异步 Future

简单地说,async 关键字可以用于创建如下类型的 Future

  • 定义函数:async fn
  • 定义 block: async {}

比如async函数:

async fn hello_world() {
    ...
}

async关键字,将函数的原型修改为返回一个Future trait object。然后将执行的结果包装在一个新的future中返回,大致相当于:

fn hello_world() -> impl Future<Output = ()> {
    async { ... }
}

注:async 代码块会实现一个匿名的 Future trait object ,包裹一个 Generator。也就是一个实现了 Future 的 GeneratorGenerator实际上是一个状态机,配合.await当每次async 代码块中任何返回 Poll::Pending则即调用generator yeild,让出执行权,一旦恢复执行,generator resume 继续执行剩余流程,当所有代码执行完,也就是状态机进入Complete,返回Poll::Ready,代表Future执行完毕。

通过 async 标记的语法块会被转换成实现了Future特征的状态机。与同步调用阻塞当前线程不同,当Future执行并遇到阻塞时,它会让出当前线程的控制权,等待其他 Future 的执行结果。

Future需要在一个执行器( executor )上运行,比如block_on 就是一个可以阻塞当前线程的执行器

// block_on 会阻塞当前线程直到指定的 Future 执行完成,这种阻塞当前线程以等待任务完成的方式较为简单、粗暴,
// 好在其它运行时的执行器(executor)会提供更加复杂的行为,例如 join 将多个 future 调度到同一个线程上执行。
use futures::executor::block_on;

async fn hello_world() {
    println!("hello, world!");
}

fn main() {
    let future = hello_world(); // 返回一个Future, 因此不会打印任何输出
    block_on(future); // 执行 Future 并等待其运行完成,此时 "hello, world!"会被打印输出
}

使用 await 等待另一个异步 Future 调用的完成

在上述代码的main函数中,我们使用block_on这个执行器等待Future的完成,让代码看上去非常像是同步代码,但是如果你要在一个async fn函数中去调用另一个async fn并等待其完成后再执行后续的代码,该如何做?例如:

use futures::executor::block_on;

async fn hello_world() {
    // 在异步函数中直接调用另一个异步函数,是否会有问题?
    hello_cat();
    println!("hello, world!");
}

async fn hello_cat() {
    println!("hello, kitty!");
}
fn main() {
    let future = hello_world();
    block_on(future);
}

这里,我们在hello_world异步函数中先调用了另一个异步函数hello_cat,然后再输出hello, world!,看看运行结果:

warning: unused implementer of `futures::Future` that must be used
 --> src/main.rs:6:5
  |
6 |     hello_cat();
  |     ^^^^^^^^^^^^
= note: futures do nothing unless you `.await` or poll them
...
hello, world!

不出所料,main函数中的future我们通过block_on函数进行了运行,但是这里的hello_cat返回的Future却没有任何人去执行它,不过好在编译器友善的给出了提示:futures do nothing unless you `.await` or poll them,两种解决方法:使用.await语法或者对Future进行轮询(poll)。

后者较为复杂,暂且不表,先来使用.await试试:

use futures::executor::block_on;

async fn hello_world() {
    hello_cat().await;
    println!("hello, world!");
}

async fn hello_cat() {
    println!("hello, kitty!");
}
fn main() {
    let future = hello_world();
    block_on(future);
}

hello_cat()添加上.await后,结果立刻大为不同:

hello, kitty!
hello, world!

输出的顺序跟代码定义的顺序完全符合,因此,我们在上面代码中使用同步的代码顺序实现了异步的执行效果,非常简单、高效,而且很好理解,未来也绝对不会有回调地狱的发生。

实际上每一个.await本身就像一个执行器,在循环中查询Future的状态。如果返回Pending,则 yield,否则退出循环,结束当前Future

代码逻辑大致如下:

loop {
    match some_future.poll() {
        Pending => yield,
        Ready(x) => break
    }
}

总之,在async fn函数中使用.await可以等待另一个异步调用的完成。但是与block_on不同,.await并不会阻塞当前的线程,而是异步的等待Future A的完成,在等待的过程中,该线程还可以继续执行其它的Future B,最终实现了并发处理的效果。

一个例子

考虑一个载歌载舞的例子,如果不用.await,我们可能会有如下实现:

use futures::executor::block_on;

struct Song {
    author: String,
    name: String,
}

async fn learn_song() -> Song {
    Song {
        author: "周杰伦".to_string(),
        name: String::from("《菊花台》"),
    }
}

async fn sing_song(song: Song) {
    println!(
        "给大家献上一首{}的{} ~ {}",
        song.author, song.name, "菊花残,满地伤~ ~"
    );
}

async fn dance() {
    println!("唱到情深处,身体不由自主的动了起来~ ~");
}

fn main() {
    let song = block_on(learn_song()); // 第一次阻塞
    block_on(sing_song(song)); // 第二次阻塞
    block_on(dance()); // 第三次阻塞
}

以上代码运行结果无疑是正确的,但需要通过连续三次阻塞去等待三个任务的完成,一次只能做一件事,实际上我们完全可以载歌载舞。

use futures::executor::block_on;

struct Song {
    author: String,
    name: String,
}

async fn learn_song() -> Song {
    Song {
        author: "周杰伦".to_string(),
        name: String::from("《菊花台》"),
    }
}

async fn sing_song(song: Song) {
    println!(
        "给大家献上一首{}的{} ~ {}",
        song.author, song.name, "菊花残,满地伤~ ~"
    );
}

async fn dance() {
    println!("唱到情深处,身体不由自主的动了起来~ ~");
}

async fn learn_and_sing() {
    // 这里使用 .await 来等待学歌的完成,但是并不会阻塞当前线程
    let song = learn_song().await;

    // 唱歌必须要在学歌之后,也就是sing_song Future 必须等待learn_song Future 完成
    sing_song(song).await;
}

async fn async_main() {
    let f1 = learn_and_sing(); // 学歌然后唱歌的 Future
    let f2 = dance(); // 跳舞的 Future

    // join!宏 可以并发的处理和等待多个 Future
    // 若 learn_and_sing Future 被阻塞,那 dance Future 可以拿过线程的所有权继续执行。若 dance 也变成阻塞状态,那 learn_and_sing 又可以再次拿回线程所有权,继续执行。
    // 若两个都被阻塞,那么 async main 会变成阻塞状态,然后让出线程所有权,并将其交给 main 函数中的 block_on 执行器
    futures::join!(f1, f2);
}

fn main() {
    block_on(async_main());
}

上面代码中,学歌和唱歌具有明显的先后顺序,但是这两者都可以跟跳舞一同存在,也就是你可以在跳舞的时候学歌,也可以在跳舞的时候唱歌。如果上面代码不使用.await,而是使用block_on(learn_song()), 那在学歌时,当前线程就会阻塞,不再可以做其它任何事,包括跳舞。

因此.await对于实现异步编程至关重要,它允许我们在同一个线程内并发的运行多个任务,而不是一个一个先后完成。

至此,对 Rust 的async/.await异步编程有了一个清晰的初步印象,后续再深度学习这背后的原理:Future和任务在底层如何被执行。

注:实际上async/.await通过一个状态机来控制代码的流程,配合执行器Executor完成协程的切换,编写异步代码不需要手动写Future及其poll方法,特别是异步逻辑的状态机也是由async自动生成,大大简化程序员的工作。

总结

async/.await是 Rust 的内置工具,用于编写看起来像同步代码的异步函数,async将一个代码区块,转换为实现称为 Future trait 的状态机,Future需要在一个 executor 上运行。而在同步方法中,调用阻塞函数将阻塞整个线程,Future 将 yield 对线程的控制权,允许其他Future运行。在async fn函数中使用.await可以等待另一个异步调用的完成。但是与block_on不同,.await并不会阻塞当前的线程,而是异步的等待Future A的完成,在等待的过程中,该线程还可以继续执行其它的Future B,最终实现了并发处理的效果。

  • Future 代表一个可在未来某个时候获取返回值的 task,在 Rust 中是惰性的,只有在被轮询(poll)时才会运行
  • async 用于创建一个Future,比如创建一个异步函数或者异步代码块
  • await 本身就像一个执行器,在循环中查询Future的状态,等待另一个 Future 的完成
  • executorFuture的管理和执行器, 比如 block_on 是一个可以阻塞当前线程的执行器
  • async 在 Rust 中使用零开销的,无需分配任何堆内存、也无需任何动态分发来使用 async
  • async/await是 Rust 语言层面支持等关键字,但是并没有内置异步调用所必需的运行时,需要引入第三方运行时实现,例如tokioasync-stdsmol

总之,async/await 是 Rust 的异步编程模型,是产生和运行并发任务的手段,async 来方便地生成 Future,await 来触发 Future 的调度和执行

参考

转载自:https://juejin.cn/post/7214007449907740732
评论
请登录