Merge pull request #319 from 1132719438/main

Some fixes in async chapter
pull/329/head
Sunface 3 years ago committed by GitHub
commit 5d2d89365d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,7 +19,7 @@ enum Poll<T> {
} }
``` ```
在上一章中,我们提到过 `Future` 需要被执行器`poll`(轮询)后才能运行,诺,这里 `poll` 就来了,通过调用该方法,可以推进 `Future` 的进一步执行,直到被切走为止( 这里不好理解,但是你只需要知道 `Future` 并不能保证在一次 `poll` 中就被执行完,后面会详解介绍)。 在上一章中,我们提到过 `Future` 需要被执行器`poll`(轮询)后才能运行,诺,这里 `poll` 就来了,通过调用该方法,可以推进 `Future` 的进一步执行,直到被切走为止( 这里不好理解,但是你只需要知道 `Future` 并不能保证在一次 `poll` 中就被执行完,后面会详解介绍)。
若在当前 `poll` 中, `Future` 可以被完成,则会返回 `Poll::Ready(result)` ,反之则返回 `Poll::Pending` 并且安排一个 `wake` 函数:当未来 `Future` 准备好进一步执行时, 该函数会被调用,然后管理该 `Future` 的执行器(例如上一章节中的`block_on`函数)会再次调用 `poll` 方法,此时 `Future` 就可以继续执行了。 若在当前 `poll` 中, `Future` 可以被完成,则会返回 `Poll::Ready(result)` ,反之则返回 `Poll::Pending` 并且安排一个 `wake` 函数:当未来 `Future` 准备好进一步执行时, 该函数会被调用,然后管理该 `Future` 的执行器(例如上一章节中的`block_on`函数)会再次调用 `poll` 方法,此时 `Future` 就可以继续执行了。
@ -224,7 +224,7 @@ impl Future for TimerFuture {
最后,再来创建一个 API 用于构建定时器和启动计时线程: 最后,再来创建一个 API 用于构建定时器和启动计时线程:
```rust ```rust
impl TimerFuture { impl TimerFuture {
/// 创建一个新的`TimeFuture`,在指定的时间结束后,该`Future`可以完成 /// 创建一个新的`TimerFuture`,在指定的时间结束后,该`Future`可以完成
pub fn new(duration: Duration) -> Self { pub fn new(duration: Duration) -> Self {
let shared_state = Arc::new(Mutex::new(SharedState { let shared_state = Arc::new(Mutex::new(SharedState {
completed: false, completed: false,
@ -257,7 +257,7 @@ Rust的 `Future` 是惰性的:只有屁股上拍一拍,它才会努力动一
执行器会管理一批 `Future` (最外层的 `ascyn` 函数),然后通过不停地 `poll` 推动它们直到完成。 最开始,执行器会先 `poll` 一次 `Future` ,后面就不会主动去 `poll` 了,而是等待 `Future` 通过调用 `wake` 函数来通知它可以继续,它才会继续去 `poll` 。这种**wake 通知然后 poll**的方式会不断重复,直到 `Future` 完成。 执行器会管理一批 `Future` (最外层的 `ascyn` 函数),然后通过不停地 `poll` 推动它们直到完成。 最开始,执行器会先 `poll` 一次 `Future` ,后面就不会主动去 `poll` 了,而是等待 `Future` 通过调用 `wake` 函数来通知它可以继续,它才会继续去 `poll` 。这种**wake 通知然后 poll**的方式会不断重复,直到 `Future` 完成。
#### 构建执行器 #### 构建执行器
下面我们将实现一个简单的执行器,它可以同时并发运行多个 `Future` 。例子中,需要用到 `futures` 包的 `ArcWake` 特征,它可以提供一个方便的途径去构建一个 `Waker` 。编辑 `Cargo.tom` ,添加下面依赖: 下面我们将实现一个简单的执行器,它可以同时并发运行多个 `Future` 。例子中,需要用到 `futures` 包的 `ArcWake` 特征,它可以提供一个方便的途径去构建一个 `Waker` 。编辑 `Cargo.toml` ,添加下面依赖:
```rust ```rust
[dependencies] [dependencies]
futures = "0.3" futures = "0.3"
@ -300,7 +300,7 @@ struct Spawner {
struct Task { struct Task {
/// 进行中的Future在未来的某个时间点会被完成 /// 进行中的Future在未来的某个时间点会被完成
/// ///
/// 按理来说`Mutext`在这里是多余的,因为我们只有一个线程来执行任务。但是由于 /// 按理来说`Mutex`在这里是多余的,因为我们只有一个线程来执行任务。但是由于
/// Rust并不聪明它无法知道`Future`只会在一个线程内被修改,并不会被跨线程修改。因此 /// Rust并不聪明它无法知道`Future`只会在一个线程内被修改,并不会被跨线程修改。因此
/// 我们需要使用`Mutex`来满足这个笨笨的编译器对线程安全的执着。 /// 我们需要使用`Mutex`来满足这个笨笨的编译器对线程安全的执着。
/// ///
@ -427,7 +427,7 @@ impl SimpleFuture for SocketRead<'_> {
关于第二点,其中一个简单粗暴的方法就是使用一个新线程不停的检查 `socket` 中是否有了数据,当有了后,就调用 `wake()` 函数。该方法确实可以满足需求,但是性能着实太低了,需要为每个阻塞的 `Future` 都创建一个单独的线程! 关于第二点,其中一个简单粗暴的方法就是使用一个新线程不停的检查 `socket` 中是否有了数据,当有了后,就调用 `wake()` 函数。该方法确实可以满足需求,但是性能着实太低了,需要为每个阻塞的 `Future` 都创建一个单独的线程!
在现实世界中,该问题往往是通过操作系统提供的 `IO` 服务来完成,例如 `linux` 、`FreeBSD` 和 `Macos` 中的 **`epoll`** `Windows` 中的 **`IOCP`**, `Fuchisa`中的 **`ports`** 等(可以通过 Rust 的跨平台包 `mio` 来使用它们)。使用它们,允许一个线程同时阻塞地去等待多个异步IO事件一旦某个事件完成就立即退出阻塞并返回数据。相关实现类似于以下代码 在现实世界中,该问题往往是通过操作系统提供的 `IO` 多路复用机制来完成,例如 `Linux` 中的 **`epoll`**`FreeBSD` 和 `MacOS` 中的 **`kqueue`** `Windows` 中的 **`IOCP`**, `Fuchisa`中的 **`ports`** 等(可以通过 Rust 的跨平台包 `mio` 来使用它们)。借助IO多路复用机制可以实现一个线程同时阻塞地去等待多个异步IO事件一旦某个事件完成就立即退出阻塞并返回数据。相关实现类似于以下代码
```rust ```rust
struct IoBlocker { struct IoBlocker {
/* ... */ /* ... */
@ -442,7 +442,7 @@ struct Event {
} }
impl IoBlocker { impl IoBlocker {
/// 创建异步IO事件的集合,这些事件是阻塞等待的 /// 创建需要阻塞等待的异步IO事件的集合
fn new() -> Self { /* ... */ } fn new() -> Self { /* ... */ }
/// 对指定的IO事件表示兴趣 /// 对指定的IO事件表示兴趣

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

Loading…
Cancel
Save