|
|
|
@ -7,7 +7,7 @@
|
|
|
|
|
|
|
|
|
|
上图并不能说 Rust 写的 `actix` 框架比 Go 的 `gin` 更好、更优秀,但是确实可以一定程度上说明 Rust 的异步性能非常的高!
|
|
|
|
|
|
|
|
|
|
简单来说,异步编程是一个[并发编程模型](https://www.zhihu.com/question/33515481/answer/2304727467),目前主流语言基本都支持了,当然,支持的方式有所不同。异步编程允许我们同时并发运行大量的任务,却仅仅需要几个甚至一个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` 是 Rust 选择的异步编程模型,下面我们来介绍下它的优缺点,以及何时适合使用。
|
|
|
|
@ -16,7 +16,7 @@
|
|
|
|
|
由于并发编程在现代社会非常重要,因此每个主流语言都对自己的并发模型进行过权衡取舍和精心设计,Rust 语言也不例外。下面的列表可以帮助大家理解不同并发模型的取舍:
|
|
|
|
|
|
|
|
|
|
- **OS线程**, 它最简单,也无需改变任何编程模型(业务/代码逻辑),因此非常适合作为语言的原生并发模型,我们在[多线程章节](../advnce/../advance/concurrency-with-threads/concurrency-parallelism.md)也提到过,Rust 就选择了原生支持线程级的并发编程。但是,这种模型也有缺点,例如线程间的同步将变得更加困难,线程间的上下文切换损耗较大。使用线程池在一定程度上可以提升性能,但是对于 IO 密集的场景来说,线程池还是不够看。
|
|
|
|
|
- **事件驱动(Event driven)**, 这个名词你可能比较陌生,如果说事件驱动常常跟回调( Callback )一起使用,相信大家就恍然大悟了。这种模型性能相当的好,但最大的问题就是存在回调地狱的风险:非线性的控制流和结果处理导致了数据流向和错误传播变得难以掌控,还会导致代码维护性和可读性的大幅降低,大名鼎鼎的 JS 曾经就存在回调地狱。
|
|
|
|
|
- **事件驱动(Event driven)**, 这个名词你可能比较陌生,如果说事件驱动常常跟回调( Callback )一起使用,相信大家就恍然大悟了。这种模型性能相当的好,但最大的问题就是存在回调地狱的风险:非线性的控制流和结果处理导致了数据流向和错误传播变得难以掌控,还会导致代码可维护性和可读性的大幅降低,大名鼎鼎的 JS 曾经就存在回调地狱。
|
|
|
|
|
- **协程(Coroutines)** 可能是目前最火的并发模型,`Go` 语言的协程设计就非常优秀,这也是 `Go` 语言能够迅速火遍全球的杀手锏之一。协程跟线程类似,无需改变编程模型,同时,它也跟 `async` 类似,可以支持大量的任务并发运行。但协程抽象层次过高,导致用户无法接触到底层的细节,这对于系统编程语言和自定义异步运行时是难以接受的
|
|
|
|
|
- **actor模型**是erlang的杀手锏之一,它将所有并发计算分割成一个一个单元,这些单元被称为 `actor` , 单元之间通过消息传递的方式进行通信和数据传递,跟分布式系统的设计理念非常相像。由于 `actor` 模型跟现实很贴近,因此它相对来说更容易实现,但是一旦遇到流控制、失败重试等场景时,就会变得不太好用
|
|
|
|
|
- **async/await**, 该模型性能高,还能支持底层编程,同时又像线程和协程那样无需过多的改变编程模型,但有得必有失,`async` 模型的问题就是内部实现机制过于复杂,对于用户来说,理解和使用起来也没有线程和协程简单,好在前者的复杂性开发者们已经帮我们封装好,而理解和使用起来不够简单,正是本章试图解决的问题。
|
|
|
|
@ -28,7 +28,7 @@
|
|
|
|
|
- 后者通过语言特性 + 标准库 + 三方库的方式实现,在你需要高并发、异步 `I/O` 时,选择它就对了
|
|
|
|
|
|
|
|
|
|
#### async: Rust vs 其它语言
|
|
|
|
|
目前已经有诸多语言都通过 `async` 的方式提供了异步编程,例如 `Javascript` ,但 `Rust` 在实现上有所区别:
|
|
|
|
|
目前已经有诸多语言都通过 `async` 的方式提供了异步编程,例如 `JavaScript` ,但 `Rust` 在实现上有所区别:
|
|
|
|
|
|
|
|
|
|
- **Future 在 Rust 中是惰性的**,只有在被轮询(`poll`)时才会运行, 因此丢弃一个 `future` 会阻止它未来再被运行, 你可以将`Future`理解为一个在未来某个时间点被调度执行的任务。
|
|
|
|
|
- **Async 在 Rust 中使用开销是零**, 意味着只有你能看到的代码(自己的代码)才有性能损耗,你看不到的(`async` 内部实现)都没有性能损耗,例如,你可以无需分配任何堆内存、也无需任何动态分发来使用 `async` ,这对于热点路径的性能有非常大的好处,正是得益于此,Rust 的异步编程性能才会这么高。
|
|
|
|
@ -41,7 +41,7 @@
|
|
|
|
|
|
|
|
|
|
`OS` 线程非常适合少量任务并发,因为线程的创建和上下文切换是非常昂贵的,甚至于空闲的线程都会消耗系统资源。虽说线程池可以有效的降低性能损耗,但是也无法彻底解决问题。当然,线程模型也有其优点,例如它不会破坏你的代码逻辑和编程模型,你之前的顺序代码,经过少量修改适配后依然可以在新线程中直接运行,同时在某些操作系统中,你还可以改变线程的优先级,这对于实现驱动程序或延迟敏感的应用(例如硬实时系统)很有帮助。
|
|
|
|
|
|
|
|
|
|
对于长时间运行的CPU密集型任务,例如密集计算,使用线程将更有优势。 这种密集任务往往会让所在的线程持续运行,任何不必要的线程切换都会带来性能损耗,因此高并发反而在此时成为了一种多余。同时你所创建的线程数应该等于CPU核心数,充分利用CPU的并行能力,甚至还可以将线程绑定到CPU核心上,进一步减少线程上下文切换。
|
|
|
|
|
对于长时间运行的CPU密集型任务,例如并行计算,使用线程将更有优势。 这种密集任务往往会让所在的线程持续运行,任何不必要的线程切换都会带来性能损耗,因此高并发反而在此时成为了一种多余。同时你所创建的线程数应该等于CPU核心数,充分利用CPU的并行能力,甚至还可以将线程绑定到CPU核心上,进一步减少线程上下文切换。
|
|
|
|
|
|
|
|
|
|
而高并发更适合 `IO` 密集型任务,例如 web 服务器、数据库连接等等网络服务,因为这些任务绝大部分时间都处于等待状态,如果使用多线程,那线程大量时间会处于无所事事的状态,再加上线程上下文切换的高昂代价,让多线程做 `IO` 密集任务变成了一件非常奢侈的事。而使用`async`,既可以有效的降低 `CPU` 和内存的负担,又可以让大量的任务并发的运行,一个任务一旦处于`IO`或者其他等待(阻塞)状态,就会被立刻切走并执行另一个任务,而这里的任务切换的性能开销要远远低于使用多线程时的线程上下文切换。
|
|
|
|
|
|
|
|
|
@ -52,8 +52,8 @@
|
|
|
|
|
总之,`async`编程并没有比多线程更好,最终还是根据你的使用场景作出合适的选择,如果无需高并发,或者也不在意线程切换带来的性能损耗,那么多线程使用起来会简单、方便的多!最后再简单总结下:
|
|
|
|
|
|
|
|
|
|
- 有大量 `IO` 任务需要并发运行时,选 `async` 模型
|
|
|
|
|
- 有部分 `IO` 任务需要并发运行时,选多线程,如果想要降低线程切换的开销,可以使用线程池
|
|
|
|
|
- 有大量 `CPU` 密集任务需要并行运行时,例如并行计算,选多线程模型,且让多线程数等于或者稍大于 `CPU` 核心数
|
|
|
|
|
- 有部分 `IO` 任务需要并发运行时,选多线程,如果想要降低线程创建和销毁的开销,可以使用线程池
|
|
|
|
|
- 有大量 `CPU` 密集任务需要并行运行时,例如并行计算,选多线程模型,且让线程数等于或者稍大于 `CPU` 核心数
|
|
|
|
|
- 无所谓时,统一选多线程
|
|
|
|
|
|
|
|
|
|
#### async和多线程的性能对比
|
|
|
|
@ -121,7 +121,7 @@ async fn get_two_sites_async() {
|
|
|
|
|
还有,你在同步( `synchronous` )代码中使用的一些语言特性在 `async` 中可能将无法再使用,而且 Rust 也不允许你在特征中声明 `async` 函数(可以通过三方库实现), 总之,你会遇到一些在同步代码中不会遇到的奇奇怪怪、形形色色的问题,不过不用担心,本章会专门用一个章节罗列这些问题,并给出相应的解决方案。
|
|
|
|
|
|
|
|
|
|
#### 编译和错误
|
|
|
|
|
在大多数情况下,`async` 中的编译错误和运行时错误和之前没啥区别,但是依然有以下几点值得注意。
|
|
|
|
|
在大多数情况下,`async` 中的编译错误和运行时错误跟之前没啥区别,但是依然有以下几点值得注意:
|
|
|
|
|
|
|
|
|
|
- 编译错误,由于 `async` 编程时需要经常使用复杂的语言特性,例如生命周期和`Pin`,因此相关的错误可能会出现的更加频繁
|
|
|
|
|
- 运行时错误,编译器会为每一个`async`函数生成状态机,这会导致在栈跟踪时会包含这些状态机的细节,同时还包含了运行时对函数的调用,因此,栈跟踪记录(例如 `panic` 时)将变得更加难以解读
|
|
|
|
@ -186,7 +186,7 @@ fn main() {
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 使用.await
|
|
|
|
|
在上述代码的`main`函数中,我们使用`block_on`这个执行器等待`Future`的完成,让代码看上去非常像是一个同步代码,但是如果你要在一个`async fn`函数中去使用另一个`async fn`并等待其完成后再进行后续的代码,该如何做?例如:
|
|
|
|
|
在上述代码的`main`函数中,我们使用`block_on`这个执行器等待`Future`的完成,让代码看上去非常像是同步代码,但是如果你要在一个`async fn`函数中去调用另一个`async fn`并等待其完成后再执行后续的代码,该如何做?例如:
|
|
|
|
|
```rust
|
|
|
|
|
use futures::executor::block_on;
|
|
|
|
|
|
|
|
|
@ -242,7 +242,7 @@ hello, kitty!
|
|
|
|
|
hello, world!
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
输出执行的顺序跟代码定义的顺序完全符合,因此,我们在上面代码中**使用同步的代码顺序实现了异步的执行效果**,非常简单、高效,而且很好理解,未来也绝对不会有回调地狱的发生。
|
|
|
|
|
输出的顺序跟代码定义的顺序完全符合,因此,我们在上面代码中**使用同步的代码顺序实现了异步的执行效果**,非常简单、高效,而且很好理解,未来也绝对不会有回调地狱的发生。
|
|
|
|
|
|
|
|
|
|
总之,在`async fn`函数中使用`.await`可以等待另一个异步调用的完成。**但是与`block_on`不同,`.await`并不会阻塞当前的线程**,而是异步的等待`Future A`的完成,在等待的过程中,该线程还可以继续执行其它的`Future B`,最终实现了并发处理的效果。
|
|
|
|
|
|
|
|
|
|