@ -4,29 +4,29 @@
并行和并发其实并不难,但是也给一些用户造成了困扰,因此我们专门开辟一个章节,用于讲清楚这两者的区别。
`Erlang` 之父[`Joe Armstrong`](https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer))( 伟大的异步编程先驱,开创一个时代的殿堂级计算机科学家,我还犹记得当年刚学到erlang时的震撼, respect!)用一张5岁小孩都能看到的图片解释了并发与并行的区别:
`Erlang` 之父[`Joe Armstrong`](https://en.wikipedia.org/wiki/Joe_Armstrong_(programmer))( 伟大的异步编程先驱,开创一个时代的殿堂级计算机科学家,我还犹记得当年刚学到 `Erlang` 时的震撼, respect! ) 用一张5岁小孩都能看到的图片解释了并发与并行的区别:
< img alt = "" src = "/img/threads-01.png" class = "center" / >
上图很直观的体现了:
- ** 并发(Concurrent)** 是多个队列使用同一个咖啡机,然后两个队列轮换着使用(未必是1:1轮换, 也可能是其它轮换规则) ,最终每个人都能接到咖啡
- ** 并发(Concurrent)** 是多个队列使用同一个咖啡机,然后两个队列轮换着使用( 未必是1:1轮换, 也可能是其它轮换规则) ,最终每个人都能接到咖啡
- ** 并行(Parallel)** 是每个队列都拥有一个咖啡机,最终也是每个人都能接到咖啡,但是效率更高,因为同时可以有两个人在接咖啡
当然,我们还可以对比下串行:只有一个队列且仅使用一台咖啡机,哪怕前面那个人接咖啡时突然发呆了几分钟,后面的人也只能等他结束才能继续接。可能有读者有疑问了,从图片来看,并发也存在这个问题啊,前面的人抽了几分钟不接 怎么办?很简单,另外一个队列的人把他推开就行了,自己队友不能在背后开枪,但是其它队的可以:)
当然,我们还可以对比下串行:只有一个队列且仅使用一台咖啡机,哪怕前面那个人接咖啡时突然发呆了几分钟,后面的人也只能等他结束才能继续接。可能有读者有疑问了,从图片来看,并发也存在这个问题啊,前面的人发呆了几分钟不接咖啡 怎么办?很简单,另外一个队列的人把他推开就行了,自己队友不能在背后开枪,但是其它队的可以:)
在正式开始之前,先给出一个结论:**并发和并行都是对"多任务" 处理的描述,其中并发是轮流处理,而并行是同时处理**。
在正式开始之前,先给出一个结论:**并发和并行都是对“多任务” 处理的描述,其中并发是轮流处理,而并行是同时处理**。
## CPU 多核
现在的个人计算机动辄拥有十来个核心(M1 Max/Intel 12代),如果使用串行的方式那真是太低调了,因此我们把各种任务简单分成多个队列,每个队列都交给一个cpu核心去执行, 当某个cpu 核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。
现在的个人计算机动辄拥有十来个核心(M1 Max/Intel 12代),如果使用串行的方式那真是太低调了,因此我们把各种任务简单分成多个队列,每个队列都交给一个 CPU 核心去执行,当某个 CPU 核心没有任务时,它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。
#### 单核心并发
那问题来了,在早期只有一个 CPU 核心时,我们的任务是怎么处理的呢?其实聪明的读者应该已经想到,是的,并发解君愁。当然,这里还得提到操作系统的多线程,正是操作系统多线程 + CPU核心, 才实现了现代化的多任务操作系统。
在 OS 级别,多线程负责管理我们的任务队列,你可以简单认为一个线程管理着一个任务队列,然后线程之间还能根据空闲度进行任务调度。我们的程序只会跟 OS 线程打交道,并不关心 CPU 到底有多少个核心,真正关心的只是 OS, 当线程把任务交给 CPU 核心去执行时,如果只有一个 CPU 核心,那么它就只能同时处理一个任务。
相信大家都看出来了:**CPU核心**对应的是上图的咖啡机,而**多个线程的任务队列**就对应的多个排队的队列,最终受限于CPU核心数, 每次只会有一个任务被处理。
相信大家都看出来了:**CPU 核心**对应的是上图的咖啡机,而**多个线程的任务队列**就对应的多个排队的队列,由于终受限于 CPU 核心数,每个队列 每次只会有一个任务被处理。
和排队一样,假如某个任务执行时间过长,就会导致用户界面的假死(相信使用 Windows 的同学或多或少都碰到过假死的问题), 那么就需要 CPU 的任务调度了(真实 CPU 的调度很复杂,我们这里做了简化),有一个调度器会按照某些条件从队列中选择任务进行执行,并且当一个任务执行时间过长时,会强行切换该任务到后台中(或者放入任务队列,真实情况很复杂!),去执行新的任务。
@ -40,7 +40,7 @@
当 CPU 核心增多到 `N` 时,那么同一时间就能有 `N` 个任务被处理,那么我们的并行度就是 `N` ,相应的处理效率也变成了单核心的 `N` 倍(实际情况并没有这么高)。
#### 多核心并发
当核心增多到`N`时,操作系统同时在进行的任务肯定远不止`N`个,而 这些任务将被放入`M`个线程队列中,接着交给`N`个CPU核心去执行, 最后实现了`M:N`的处理模型,在这种情况下,**并发跟并行时同时在发生的,所有用户任务从表面来看都在并发的运行,其实实际上,同一时刻只有`N`个任务能被同时并行的处理**。
当核心增多到 `N` 时,操作系统同时在进行的任务肯定远不止 `N` 个,这些任务将被放入 `M` 个线程队列中,接着交给 `N` 个CPU核心去执行, 最后实现了 `M:N` 的处理模型,在这种情况下,**并发跟并行时同时在发生的,所有用户任务从表面来看都在并发的运行,其实实际上,同一时刻只有 `N` 个任务能被同时并行的处理**。
看到这里,相信大家已经明白两者的区别,那么我们下面给出一个正式的定义(该定义摘选自< < 并发的艺术>>)。
@ -54,13 +54,13 @@
## 编程语言的并发模型
如果大家学过其它语言的多线程,可能就知道不同语言对于线程的实现可能大相径庭:
如果大家学过其它语言的多线程,可能就知道不同语言对于线程的实现可能大相径庭:
- 由于操作系统提供了创建线程的API, 因此部分语言会直接调用该API来创建线程, 因此最终程序内的线程数和该程序占用的操作系统线程数相等, 一般称之为**1:1线程模型**, 例如Rust
- 还有些语言在内部实现了自己的线程模型(绿色线程、协程), 程序内部的M个线程最后会以某种映射方式使用N个操作系统线程去运行, 因此称之为**M:N线程模型**, 其中M和N 并没有特定的彼此限制关系。一个典型的代表就是Go语言
- 还有些语言使用了Actor模型, 基于消息传递进行并发, 例如Erlang语言
- 由于操作系统提供了创建线程的 API, 因此部分语言会直接调用该 API 来创建线程,因此最终程序内的线程数和该程序占用的操作系统线程数相等,一般称之为**1:1 线程模型**,例如 Rust。
- 还有些语言在内部实现了自己的线程模型(绿色线程、协程),程序内部的 M 个线程最后会以某种映射方式使用 N 个操作系统线程去运行,因此称之为**M:N 线程模型**,其中 M 和 N 并没有特定的彼此限制关系。一个典型的代表就是 Go 语言。
- 还有些语言使用了 Actor 模型,基于消息传递进行并发,例如 Erlang 语言。
总之,每一种模型都有其优缺点及选择上的权衡,而Rust在设计时考虑的权衡就是运行时(runtime)。出于Rust的系统级使用场景, 且要保证调用C 时的极致性能,它最终选择了尽量小的运行时实现。
总之,每一种模型都有其优缺点及选择上的权衡,而 Rust 在设计时考虑的权衡就是运行时(Runtime)。出于 Rust 的系统级使用场景,且要保证调用 C 时的极致性能,它最终选择了尽量小的运行时实现。
> 运行时是那些会被打包到所有程序可执行文件中的 Rust 代码,根据每个语言的设计权衡,运行时虽然有大有小(例如 Go 语言由于实现了协程和 GC, 运行时相对就会更大一些),但是除了汇编之外,每个语言都拥有它。小运行时的其中一个好处在于最终编译出的可执行文件会相对较小,同时也让该语言更容易被其它语言引入使用。