@ -1,4 +1,5 @@
# Async 编程简介
# Async 编程简介
众所周知, Rust 可以让我们写出性能高且安全的软件,那么异步编程这块儿呢?是否依然在高性能的同时保证了安全?
众所周知, Rust 可以让我们写出性能高且安全的软件,那么异步编程这块儿呢?是否依然在高性能的同时保证了安全?
我们先通过一张 web 框架性能对比图来感受下 Rust 异步编程的性能:
我们先通过一张 web 框架性能对比图来感受下 Rust 异步编程的性能:
@ -10,9 +11,11 @@
简单来说,异步编程是一个[并发编程模型](https://course.rs/advance/concurrency-with-threads/concurrency-parallelism.html),目前主流语言基本都支持了,当然,支持的方式有所不同。异步编程允许我们同时并发运行大量的任务,却仅仅需要几个甚至一个 OS 线程或 CPU 核心,现代化的异步编程在使用体验上跟同步编程也几无区别,例如 Go 语言的 `go` 关键字,也包括我们后面将介绍的 `async/await` 语法,该语法是 `JavaScript` 和 `Rust` 的核心特性之一。
简单来说,异步编程是一个[并发编程模型](https://course.rs/advance/concurrency-with-threads/concurrency-parallelism.html),目前主流语言基本都支持了,当然,支持的方式有所不同。异步编程允许我们同时并发运行大量的任务,却仅仅需要几个甚至一个 OS 线程或 CPU 核心,现代化的异步编程在使用体验上跟同步编程也几无区别,例如 Go 语言的 `go` 关键字,也包括我们后面将介绍的 `async/await` 语法,该语法是 `JavaScript` 和 `Rust` 的核心特性之一。
## async 简介
## async 简介
`async` 是 Rust 选择的异步编程模型,下面我们来介绍下它的优缺点,以及何时适合使用。
`async` 是 Rust 选择的异步编程模型,下面我们来介绍下它的优缺点,以及何时适合使用。
#### async vs 其它并发模型
#### async vs 其它并发模型
由于并发编程在现代社会非常重要, 因此每个主流语言都对自己的并发模型进行过权衡取舍和精心设计, Rust 语言也不例外。下面的列表可以帮助大家理解不同并发模型的取舍:
由于并发编程在现代社会非常重要, 因此每个主流语言都对自己的并发模型进行过权衡取舍和精心设计, Rust 语言也不例外。下面的列表可以帮助大家理解不同并发模型的取舍:
- **OS 线程** , 它最简单,也无需改变任何编程模型(业务/代码逻辑),因此非常适合作为语言的原生并发模型,我们在[多线程章节](../advnce/../advance/concurrency-with-threads/concurrency-parallelism.md)也提到过, Rust 就选择了原生支持线程级的并发编程。但是,这种模型也有缺点,例如线程间的同步将变得更加困难,线程间的上下文切换损耗较大。使用线程池在一定程度上可以提升性能,但是对于 IO 密集的场景来说,线程池还是不够看。
- **OS 线程** , 它最简单,也无需改变任何编程模型(业务/代码逻辑),因此非常适合作为语言的原生并发模型,我们在[多线程章节](../advnce/../advance/concurrency-with-threads/concurrency-parallelism.md)也提到过, Rust 就选择了原生支持线程级的并发编程。但是,这种模型也有缺点,例如线程间的同步将变得更加困难,线程间的上下文切换损耗较大。使用线程池在一定程度上可以提升性能,但是对于 IO 密集的场景来说,线程池还是不够看。
@ -21,13 +24,13 @@
- **actor 模型** 是 erlang 的杀手锏之一,它将所有并发计算分割成一个一个单元,这些单元被称为 `actor` , 单元之间通过消息传递的方式进行通信和数据传递,跟分布式系统的设计理念非常相像。由于 `actor` 模型跟现实很贴近,因此它相对来说更容易实现,但是一旦遇到流控制、失败重试等场景时,就会变得不太好用
- **actor 模型** 是 erlang 的杀手锏之一,它将所有并发计算分割成一个一个单元,这些单元被称为 `actor` , 单元之间通过消息传递的方式进行通信和数据传递,跟分布式系统的设计理念非常相像。由于 `actor` 模型跟现实很贴近,因此它相对来说更容易实现,但是一旦遇到流控制、失败重试等场景时,就会变得不太好用
- **async/await** , 该模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,但有得必有失,`async` 模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用起来也没有线程和协程简单,好在前者的复杂性开发者们已经帮我们封装好,而理解和使用起来不够简单,正是本章试图解决的问题。
- **async/await** , 该模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,但有得必有失,`async` 模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用起来也没有线程和协程简单,好在前者的复杂性开发者们已经帮我们封装好,而理解和使用起来不够简单,正是本章试图解决的问题。
总之, Rust 经过权衡取舍后,最终选择了同时提供多线程编程和 async 编程:
总之, Rust 经过权衡取舍后,最终选择了同时提供多线程编程和 async 编程:
- 前者通过标准库实现,当你无需那么高的并发时,例如需要并行计算时,可以选择它,优点是线程内的代码执行效率更高、实现更直观更简单,这块内容已经在多线程章节进行过深入讲解,不再赘述
- 前者通过标准库实现,当你无需那么高的并发时,例如需要并行计算时,可以选择它,优点是线程内的代码执行效率更高、实现更直观更简单,这块内容已经在多线程章节进行过深入讲解,不再赘述
- 后者通过语言特性 + 标准库 + 三方库的方式实现,在你需要高并发、异步 `I/O` 时,选择它就对了
- 后者通过语言特性 + 标准库 + 三方库的方式实现,在你需要高并发、异步 `I/O` 时,选择它就对了
#### async: Rust vs 其它语言
#### async: Rust vs 其它语言
目前已经有诸多语言都通过 `async` 的方式提供了异步编程,例如 `JavaScript` ,但 `Rust` 在实现上有所区别:
目前已经有诸多语言都通过 `async` 的方式提供了异步编程,例如 `JavaScript` ,但 `Rust` 在实现上有所区别:
- **Future 在 Rust 中是惰性的** ,只有在被轮询(`poll`)时才会运行, 因此丢弃一个 `future` 会阻止它未来再被运行, 你可以将`Future`理解为一个在未来某个时间点被调度执行的任务。
- **Future 在 Rust 中是惰性的** ,只有在被轮询(`poll`)时才会运行, 因此丢弃一个 `future` 会阻止它未来再被运行, 你可以将`Future`理解为一个在未来某个时间点被调度执行的任务。
@ -35,8 +38,8 @@
- **Rust 没有内置异步调用所必须的运行时** , 但是无需担心, Rust 社区生态中已经提供了非常优异的运行时实现,例如大明星 [`tokio` ](https://tokio.rs )
- **Rust 没有内置异步调用所必须的运行时** , 但是无需担心, Rust 社区生态中已经提供了非常优异的运行时实现,例如大明星 [`tokio` ](https://tokio.rs )
- ** 运行时同时支持单线程和多线程**,这两者拥有各自的优缺点, 稍后会讲
- ** 运行时同时支持单线程和多线程**,这两者拥有各自的优缺点, 稍后会讲
#### Rust: async vs 多线程
#### Rust: async vs 多线程
虽然 `async` 和多线程都可以实现并发编程,后者甚至还能通过线程池来增强并发能力,但是这两个方式并不互通,从一个方式切换成另一个需要大量的代码重构工作,因此提前为自己的项目选择适合的并发模型就变得至关重要。
虽然 `async` 和多线程都可以实现并发编程,后者甚至还能通过线程池来增强并发能力,但是这两个方式并不互通,从一个方式切换成另一个需要大量的代码重构工作,因此提前为自己的项目选择适合的并发模型就变得至关重要。
`OS` 线程非常适合少量任务并发,因为线程的创建和上下文切换是非常昂贵的,甚至于空闲的线程都会消耗系统资源。虽说线程池可以有效的降低性能损耗,但是也无法彻底解决问题。当然,线程模型也有其优点,例如它不会破坏你的代码逻辑和编程模型,你之前的顺序代码,经过少量修改适配后依然可以在新线程中直接运行,同时在某些操作系统中,你还可以改变线程的优先级,这对于实现驱动程序或延迟敏感的应用(例如硬实时系统)很有帮助。
`OS` 线程非常适合少量任务并发,因为线程的创建和上下文切换是非常昂贵的,甚至于空闲的线程都会消耗系统资源。虽说线程池可以有效的降低性能损耗,但是也无法彻底解决问题。当然,线程模型也有其优点,例如它不会破坏你的代码逻辑和编程模型,你之前的顺序代码,经过少量修改适配后依然可以在新线程中直接运行,同时在某些操作系统中,你还可以改变线程的优先级,这对于实现驱动程序或延迟敏感的应用(例如硬实时系统)很有帮助。
@ -59,14 +62,16 @@
#### async 和多线程的性能对比
#### async 和多线程的性能对比
| 操作 | async | 线程 |
| 操作 | async | 线程 |
| ---- | ----- | ---- |
| -------- | -- ------ | ---- ---- |
| 创建 | 0.3 微秒 | 17 微秒 |
| 创建 | 0.3 微秒 | 17 微秒 |
| 线程切换 | 0.2 微秒 | 1.7 微秒 |
| 线程切换 | 0.2 微秒 | 1.7 微秒 |
可以看出,`async` 在线程切换的开销显著低于多线程,对于 IO 密集的场景,这种性能开销累计下来会非常可怕!
可以看出,`async` 在线程切换的开销显著低于多线程,对于 IO 密集的场景,这种性能开销累计下来会非常可怕!
#### 一个例子
#### 一个例子
在大概理解`async`后,我们再来看一个简单的例子。如果想并发的下载文件,你可以使用多线程如下实现:
在大概理解`async`后,我们再来看一个简单的例子。如果想并发的下载文件,你可以使用多线程如下实现:
```rust
```rust
fn get_two_sites() {
fn get_two_sites() {
// 创建两个新线程执行任务
// 创建两个新线程执行任务
@ -80,6 +85,7 @@ fn get_two_sites() {
```
```
如果是在一个小项目中简单的去下载文件,这么写没有任何问题,但是一旦下载文件的并发请求多起来,那一个下载任务占用一个线程的模式就太重了,会很容易成为程序的瓶颈。好在,我们可以使用`async`的方式来解决:
如果是在一个小项目中简单的去下载文件,这么写没有任何问题,但是一旦下载文件的并发请求多起来,那一个下载任务占用一个线程的模式就太重了,会很容易成为程序的瓶颈。好在,我们可以使用`async`的方式来解决:
```rust
```rust
async fn get_two_sites_async() {
async fn get_two_sites_async() {
// 创建两个不同的`future`,你可以把`future`理解为未来某个时刻会被执行的计划任务
// 创建两个不同的`future`,你可以把`future`理解为未来某个时刻会被执行的计划任务
@ -97,6 +103,7 @@ async fn get_two_sites_async() {
事实上,`async` 和多线程并不是二选一,在同一应用中,可以根据情况两者一起使用,当然,我们还可以使用其它的并发模型,例如上面提到事件驱动模型,前提是有三方库提供了相应的实现。
事实上,`async` 和多线程并不是二选一,在同一应用中,可以根据情况两者一起使用,当然,我们还可以使用其它的并发模型,例如上面提到事件驱动模型,前提是有三方库提供了相应的实现。
## Async Rust 当前的进展
## Async Rust 当前的进展
简而言之, Rust 语言的 `async` 目前还没有达到多线程的成熟度,其中一部分内容还在不断进化中,当然,这并不影响我们在生产级项目中使用,因为社区中还有 `tokio` 这种大杀器。
简而言之, Rust 语言的 `async` 目前还没有达到多线程的成熟度,其中一部分内容还在不断进化中,当然,这并不影响我们在生产级项目中使用,因为社区中还有 `tokio` 这种大杀器。
使用 `async` 时,你会遇到好的,也会遇到不好的,例如:
使用 `async` 时,你会遇到好的,也会遇到不好的,例如:
@ -111,6 +118,7 @@ async fn get_two_sites_async() {
不过好在,这些进化早晚会彻底稳定成熟,而且在实际项目中,我们往往会使用成熟的三方库,例如`tokio`,因此可以避免一些类似的问题,但是对于本章的学习来说,`async` 的一些难点还是我们必须要去面对和征服的。
不过好在,这些进化早晚会彻底稳定成熟,而且在实际项目中,我们往往会使用成熟的三方库,例如`tokio`,因此可以避免一些类似的问题,但是对于本章的学习来说,`async` 的一些难点还是我们必须要去面对和征服的。
#### 语言和库的支持
#### 语言和库的支持
`async` 的底层实现非常复杂,且会导致编译后文件体积显著增加,因此 Rust 没有选择像 Go 语言那样内置了完整的特性和运行时,而是选择了通过 Rust 语言提供了必要的特性支持,再通过社区来提供 `async` 运行时的支持。 因此要完整的使用 `async` 异步编程,你需要依赖以下特性和外部库:
`async` 的底层实现非常复杂,且会导致编译后文件体积显著增加,因此 Rust 没有选择像 Go 语言那样内置了完整的特性和运行时,而是选择了通过 Rust 语言提供了必要的特性支持,再通过社区来提供 `async` 运行时的支持。 因此要完整的使用 `async` 异步编程,你需要依赖以下特性和外部库:
- 所必须的特征(例如 `Future` )、类型和函数,由标准库提供实现
- 所必须的特征(例如 `Future` )、类型和函数,由标准库提供实现
@ -121,19 +129,21 @@ async fn get_two_sites_async() {
还有,你在同步( `synchronous` )代码中使用的一些语言特性在 `async` 中可能将无法再使用,而且 Rust 也不允许你在特征中声明 `async` 函数(可以通过三方库实现), 总之,你会遇到一些在同步代码中不会遇到的奇奇怪怪、形形色色的问题,不过不用担心,本章会专门用一个章节罗列这些问题,并给出相应的解决方案。
还有,你在同步( `synchronous` )代码中使用的一些语言特性在 `async` 中可能将无法再使用,而且 Rust 也不允许你在特征中声明 `async` 函数(可以通过三方库实现), 总之,你会遇到一些在同步代码中不会遇到的奇奇怪怪、形形色色的问题,不过不用担心,本章会专门用一个章节罗列这些问题,并给出相应的解决方案。
#### 编译和错误
#### 编译和错误
在大多数情况下,`async` 中的编译错误和运行时错误跟之前没啥区别,但是依然有以下几点值得注意:
在大多数情况下,`async` 中的编译错误和运行时错误跟之前没啥区别,但是依然有以下几点值得注意:
- 编译错误,由于 `async` 编程时需要经常使用复杂的语言特性,例如生命周期和`Pin`,因此相关的错误可能会出现的更加频繁
- 编译错误,由于 `async` 编程时需要经常使用复杂的语言特性,例如生命周期和`Pin`,因此相关的错误可能会出现的更加频繁
- 运行时错误,编译器会为每一个`async`函数生成状态机,这会导致在栈跟踪时会包含这些状态机的细节,同时还包含了运行时对函数的调用,因此,栈跟踪记录(例如 `panic` 时)将变得更加难以解读
- 运行时错误,编译器会为每一个`async`函数生成状态机,这会导致在栈跟踪时会包含这些状态机的细节,同时还包含了运行时对函数的调用,因此,栈跟踪记录(例如 `panic` 时)将变得更加难以解读
- 一些隐蔽的错误也可能发生,例如在一个 `async` 上下文中去调用一个阻塞的函数,或者没有正确的实现 `Future` 特征都有可能导致这种错误。这种错误可能会悄无声息的通过编译检查甚至有时候会通过单元测试。好在一旦你深入学习并掌握了本章的内容和 `async` 原理,可以有效的降低遇到这些错误的概率
- 一些隐蔽的错误也可能发生,例如在一个 `async` 上下文中去调用一个阻塞的函数,或者没有正确的实现 `Future` 特征都有可能导致这种错误。这种错误可能会悄无声息的通过编译检查甚至有时候会通过单元测试。好在一旦你深入学习并掌握了本章的内容和 `async` 原理,可以有效的降低遇到这些错误的概率
#### 兼容性考虑
#### 兼容性考虑
异步代码和同步代码并不总能和睦共处。例如,我们无法在一个同步函数中去调用一个 `async` 异步函数,同步和异步代码也往往使用不同的设计模式,这些都会导致两者融合上的困难。
异步代码和同步代码并不总能和睦共处。例如,我们无法在一个同步函数中去调用一个 `async` 异步函数,同步和异步代码也往往使用不同的设计模式,这些都会导致两者融合上的困难。
甚至于有时候,异步代码之间也存在类似的问题,如果一个库依赖于特定的 `async` 运行时来运行,那么这个库非常有必要告诉它的用户,它用了这个运行时。否则一旦用户选了不同的或不兼容的运行时,就会导致不可预知的麻烦。
甚至于有时候,异步代码之间也存在类似的问题,如果一个库依赖于特定的 `async` 运行时来运行,那么这个库非常有必要告诉它的用户,它用了这个运行时。否则一旦用户选了不同的或不兼容的运行时,就会导致不可预知的麻烦。
#### 性能特性
#### 性能特性
`async` 代码的性能主要取决于你使用的 `async` 运行时,好在这些运行时都经过了精心的设计,在你能遇到的绝大多数场景中,它们都能拥有非常棒的性能表现。
`async` 代码的性能主要取决于你使用的 `async` 运行时,好在这些运行时都经过了精心的设计,在你能遇到的绝大多数场景中,它们都能拥有非常棒的性能表现。
但是世事皆有例外。目前主流的 `async` 运行时几乎都使用了多线程实现,相比单线程虽然增加了并发表现,但是对于执行性能会有所损失,因为多线程实现会有同步和切换上的性能开销,若你需要极致的顺序执行性能,那么 `async` 目前并不是一个好的选择。
但是世事皆有例外。目前主流的 `async` 运行时几乎都使用了多线程实现,相比单线程虽然增加了并发表现,但是对于执行性能会有所损失,因为多线程实现会有同步和切换上的性能开销,若你需要极致的顺序执行性能,那么 `async` 目前并不是一个好的选择。
@ -142,20 +152,23 @@ async fn get_two_sites_async() {
以上的两个需求,目前的 `async` 运行时并不能很好的支持,在未来可能会有更好的支持,但在此之前,我们可以尝试用多线程解决。
以上的两个需求,目前的 `async` 运行时并不能很好的支持,在未来可能会有更好的支持,但在此之前,我们可以尝试用多线程解决。
## async/.await 简单入门
## async/.await 简单入门
`async/.await` 是 Rust 内置的语言特性,可以让我们用同步的方式去编写异步的代码。
`async/.await` 是 Rust 内置的语言特性,可以让我们用同步的方式去编写异步的代码。
通过 `async` 标记的语法块会被转换成实现了`Future`特征的状态机。 与同步调用阻塞当前线程不同,当`Future`执行并遇到阻塞时,它会让出当前线程的控制权,这样其它的`Future`就可以在该线程中运行,这种方式完全不会导致当前线程的阻塞。
通过 `async` 标记的语法块会被转换成实现了`Future`特征的状态机。 与同步调用阻塞当前线程不同,当`Future`执行并遇到阻塞时,它会让出当前线程的控制权,这样其它的`Future`就可以在该线程中运行,这种方式完全不会导致当前线程的阻塞。
下面我们来通过例子学习 `async/.await` 关键字该如何使用,在开始之前,需要先引入 `futures` 包。编辑 `Cargo.toml` 文件并添加以下内容:
下面我们来通过例子学习 `async/.await` 关键字该如何使用,在开始之前,需要先引入 `futures` 包。编辑 `Cargo.toml` 文件并添加以下内容:
```toml
```toml
[dependencies]
[dependencies]
futures = "0.3"
futures = "0.3"
```
```
#### 使用 async
#### 使用 async
首先,使用 `async fn` 语法来创建一个异步函数:
首先,使用 `async fn` 语法来创建一个异步函数:
```rust
```rust
async fn do_something() {
async fn do_something() {
println!("go go go !");
println!("go go go !");
@ -163,6 +176,7 @@ async fn do_something() {
```
```
需要注意,**异步函数的返回值是一个 `Future` **,若直接调用该函数,不会输出任何结果,因为 `Future` 还未被执行:
需要注意,**异步函数的返回值是一个 `Future` **,若直接调用该函数,不会输出任何结果,因为 `Future` 还未被执行:
```rust
```rust
fn main() {
fn main() {
do_something();
do_something();
@ -170,6 +184,7 @@ fn main() {
```
```
运行后,`go go go`并没有打印,同时编译器给予一个提示:`warning: unused implementer of Future that must be used`,告诉我们 `Future` 未被使用,那么到底该如何使用?答案是使用一个执行器( `executor` ):
运行后,`go go go`并没有打印,同时编译器给予一个提示:`warning: unused implementer of Future that must be used`,告诉我们 `Future` 未被使用,那么到底该如何使用?答案是使用一个执行器( `executor` ):
```rust
```rust
// `block_on` 会阻塞当前线程直到指定的`Future`执行完成,这种阻塞当前线程以等待任务完成的方式较为简单、粗暴,
// `block_on` 会阻塞当前线程直到指定的`Future`执行完成,这种阻塞当前线程以等待任务完成的方式较为简单、粗暴,
// 好在其它运行时的执行器(executor)会提供更加复杂的行为,例如将多个`future`调度到同一个线程上执行。
// 好在其它运行时的执行器(executor)会提供更加复杂的行为,例如将多个`future`调度到同一个线程上执行。
@ -186,7 +201,9 @@ fn main() {
```
```
#### 使用.await
#### 使用.await
在上述代码的`main`函数中,我们使用`block_on`这个执行器等待`Future`的完成,让代码看上去非常像是同步代码,但是如果你要在一个`async fn`函数中去调用另一个`async fn`并等待其完成后再执行后续的代码,该如何做?例如:
在上述代码的`main`函数中,我们使用`block_on`这个执行器等待`Future`的完成,让代码看上去非常像是同步代码,但是如果你要在一个`async fn`函数中去调用另一个`async fn`并等待其完成后再执行后续的代码,该如何做?例如:
```rust
```rust
use futures::executor::block_on;
use futures::executor::block_on;
@ -205,6 +222,7 @@ fn main() {
```
```
这里,我们在`hello_world`异步函数中先调用了另一个异步函数`hello_cat`,然后再输出`hello, world!`,看看运行结果:
这里,我们在`hello_world`异步函数中先调用了另一个异步函数`hello_cat`,然后再输出`hello, world!`,看看运行结果:
```console
```console
warning: unused implementer of `futures::Future` that must be used
warning: unused implementer of `futures::Future` that must be used
--> src/main.rs:6:5
--> src/main.rs:6:5
@ -219,6 +237,7 @@ hello, world!
不出所料,`main`函数中的`future`我们通过`block_on`函数进行了运行,但是这里的`hello_cat`返回的`Future`却没有任何人去执行它,不过好在编译器友善的给出了提示:`futures do nothing unless you .await or poll them `,两种解决方法:使用` .await`语法或者对`Future`进行轮询(`poll`)。
不出所料,`main`函数中的`future`我们通过`block_on`函数进行了运行,但是这里的`hello_cat`返回的`Future`却没有任何人去执行它,不过好在编译器友善的给出了提示:`futures do nothing unless you .await or poll them `,两种解决方法:使用` .await`语法或者对`Future`进行轮询(`poll`)。
后者较为复杂,暂且不表,先来使用`.await`试试:
后者较为复杂,暂且不表,先来使用`.await`试试:
```rust
```rust
use futures::executor::block_on;
use futures::executor::block_on;
@ -237,6 +256,7 @@ fn main() {
```
```
为`hello_cat()`添加上`.await`后,结果立刻大为不同:
为`hello_cat()`添加上`.await`后,结果立刻大为不同:
```console
```console
hello, kitty!
hello, kitty!
hello, world!
hello, world!
@ -247,7 +267,9 @@ hello, world!
总之,在`async fn`函数中使用`.await`可以等待另一个异步调用的完成。**但是与`block_on`不同,`.await`并不会阻塞当前的线程**,而是异步的等待`Future A`的完成,在等待的过程中,该线程还可以继续执行其它的`Future B`,最终实现了并发处理的效果。
总之,在`async fn`函数中使用`.await`可以等待另一个异步调用的完成。**但是与`block_on`不同,`.await`并不会阻塞当前的线程**,而是异步的等待`Future A`的完成,在等待的过程中,该线程还可以继续执行其它的`Future B`,最终实现了并发处理的效果。
#### 一个例子
#### 一个例子
考虑一个载歌载舞的例子,如果不用`.await`,我们可能会有如下实现:
考虑一个载歌载舞的例子,如果不用`.await`,我们可能会有如下实现:
```rust
```rust
async fn learn_song() -> Song { /* ... */ }
async fn learn_song() -> Song { /* ... */ }
async fn sing_song(song: Song) { /* ... */ }
async fn sing_song(song: Song) { /* ... */ }
@ -261,6 +283,7 @@ fn main() {
```
```
当然,以上代码运行结果无疑是正确的,但。。。它的性能何在?需要通过连续三次阻塞去等待三个任务的完成,一次只能做一件事,实际上我们完全可以载歌载舞啊:
当然,以上代码运行结果无疑是正确的,但。。。它的性能何在?需要通过连续三次阻塞去等待三个任务的完成,一次只能做一件事,实际上我们完全可以载歌载舞啊:
```rust
```rust
async fn sing_song(song: Song) { /* ... */ }
async fn sing_song(song: Song) { /* ... */ }
async fn learn_and_sing() {
async fn learn_and_sing() {
@ -287,8 +310,6 @@ fn main() {
上面代码中,学歌和唱歌具有明显的先后顺序,但是这两者都可以跟跳舞一同存在,也就是你可以在跳舞的时候学歌,也可以在跳舞的时候唱歌。如果上面代码不使用`.await`,而是使用`block_on(learn_song())`, 那在学歌时,当前线程就会阻塞,不再可以做其它任何事,包括跳舞。
上面代码中,学歌和唱歌具有明显的先后顺序,但是这两者都可以跟跳舞一同存在,也就是你可以在跳舞的时候学歌,也可以在跳舞的时候唱歌。如果上面代码不使用`.await`,而是使用`block_on(learn_song())`, 那在学歌时,当前线程就会阻塞,不再可以做其它任何事,包括跳舞。
因此`.await`对于实现异步编程至关重要,它允许我们在同一个线程内并发的运行多个任务,而不是一个一个先后完成。若大家看到这里还是不太明白,强烈建议回头再仔细看一遍,同时亲自上手修改代码试试效果。
因此`.await`对于实现异步编程至关重要,它允许我们在同一个线程内并发的运行多个任务,而不是一个一个先后完成。若大家看到这里还是不太明白,强烈建议回头再仔细看一遍,同时亲自上手修改代码试试效果。
至此,读者应该对 Rust 的`async/.await`异步编程有了一个清晰的初步印象,下面让我们一起来看看这背后的原理:`Future`和任务在底层如何被执行。
至此,读者应该对 Rust 的`async/.await`异步编程有了一个清晰的初步印象,下面让我们一起来看看这背后的原理:`Future`和任务在底层如何被执行。