Rust 学习笔记(43)-异步编程async

参考章节《Rust语言圣经(Rust Course)》第4.1章 Rust 异步编程

什么是异步编程?

异步编程允许我们同时并发运行大量的任务,却仅仅需要几个甚至一个 OS 线程或 CPU 核心

线程和异步编程的区别?

OS 线程非常适合少量任务并发,因为线程的创建和上下文切换是非常昂贵的,甚至于空闲的线程都会消耗系统资源。

虽然 async 和多线程都可以实现并发编程,后者甚至还能通过线程池来增强并发能力,但是这两个方式并不互通
当从一个方式切换成另一个需要大量的代码重构工作(代价比较大),因此提前为自己的项目选择适合的并发模型就变得至关重要(提前选择好并发模型至关重要)

Rust 选择了 async/await, 该模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,
但有得必有失,async 模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用起来也没有线程和协程简单,
好在前者的复杂性开发者们已经帮我们封装好

事实上 async 底层也是基于线程实现,但是它基于线程封装了一个运行时
可以将多个任务映射到少量线程上,然后将线程切换变成了任务切换,后者仅仅是内存中的访问,因此要高效的多。

该使用哪种并发模型?

  • 对于长时间运行的 CPU 密集型任务,例如并行计算,使用线程将更有优势。
  • 有部分 IO 任务需要并发运行时,选多线程,如果想要降低线程创建和销毁的开销,可以使用线程池
  • 有大量 IO 任务需要并发运行时(IO密集型),选 async 模型

总结一下,只有当有大量IO密集型任务时才选async,否则统一选多线程

事实上,async 和多线程并不是二选一,在同一应用中,可以根据情况两者一起使用

async/.await 简单入门

在开始之前,需要先引入 futures 包。编辑 Cargo.toml 文件并添加以下内容:

1
2
[dependencies]
futures = "0.3"

定义异步函数使用 async fn 关键字

1
async fn do_something() { /* ... */ }

需要注意,异步函数的返回值是一个 Future,若直接调用该函数,不会输出任何结果,因为 Future 还未被执行:

1
fn main() { do_something(); }

我们可以使用一个 block_on 执行器来执行 Future

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// `block_on`会阻塞当前线程直到指定的`Future`执行完成,这种阻塞当前线程以等待任务完成的方式较为简单、粗暴,
// 好在其它运行时的执行器(executor)会提供更加复杂的行为,例如将多个`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

如果你需要在一个异步函数中调用另一个异步函数就需要用.await

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use futures::executor::block_on;

async fn hello_world() {
    hello_cat().await; // 调用 hello_cat 异步函数,.await 可以等待异步调用的完成。
    println!("hello, world!");
}

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

block_on 不同,.await 并不会阻塞当前的线程,而是异步的等待Future的完成,在等待的过程中,该线程还可以继续执行其它的Future,最终实现了并发处理的效果。

1
2
hello, kitty!
hello, world!

一个简单的例子

我们有如下三个异步函数

1
2
3
async fn learn_song() -> Song { /* ... */ } // 学习歌曲
async fn sing_song(song: Song) { /* ... */ } // 唱一首歌
async fn dance() { /* ... */ } // 跳舞

我们可以这么调用

1
2
3
4
5
6
7
use futures::executor::block_on;

fn main() {
    let song = block_on(learn_song()); // 先调用学习歌曲
    block_on(sing_song(song)); // 再唱这首歌
    block_on(dance()); // 再跳舞
}

以上代码虽然是正确的,但我们仔细观察,发现跳舞和唱歌并不冲突,我们完全可以边跳舞边唱歌啊,因此我们可以利用 .await 来实现这个效果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
async fn learn_and_sing() {
    let song = learn_song().await;
    sing_song(song).await;
}

async fn async_main() {
    let f1 = learn_and_sing();
    let f2 = dance();
    // `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());
}

总结一下

  1. async适用于大量IO密集型任务,其它情况统一选择OS多线程模型
  2. async模型并不一定优于OS多线程模型,各自有各自的适用场景
  3. asyncOS多线程并不是二选一,在同一应用中,可以根据情况两者一起使用
  4. block_on是一个执行器,它会阻塞当前线程直到指定的Future执行完成
  5. .await 并不会阻塞当前的线程,而是异步的等待Future的完成,在等待的过程中,该线程还可以继续执行其它的Future
  6. futures::join!可以并发的处理和等待多个Future
updatedupdated2025-03-012025-03-01