You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

7.6 KiB

并发和并行

并发是同一时间应对多件事情的能力 - Rob Pike

并行和并发其实并不难,但是也给一些用户造成了困扰,因此我们专门开辟一个章节,用于讲清楚这两者的区别。

Erlang之父Joe Armstrong(伟大的异步编程先驱开创一个时代的殿堂级计算机科学家我还犹记得当年刚学到erlang时的震撼respect!)用一张5岁小孩都能看到的图片解释了并发与并行的区别:

上图很直观的体现了:

  • 并发(Concurrent) 是多个队列使用同一个咖啡机,然后两个队列轮换着使用(未必是1:1轮换也可能是其它轮换规则),最终每个人都能接到咖啡
  • 并行(Parallel) 是每个队列都拥有一个咖啡机,最终也是每个人都能接到咖啡,但是效率更高,因为同时可以有两个人在接咖啡

当然,我们还可以对比下串行:只有一个队列且仅使用一台咖啡机,哪怕前面那个人接咖啡时突然发呆了几分钟,后面的人也只能等他结束才能继续接。可能有读者有疑问了,从图片来看,并发也存在这个问题啊,前面的人抽了几分钟不接怎么办?很简单,另外一个队列的人把他推开就行了,自己队友不能在背后开枪,但是其它队的可以:)

在正式开始之前,先给出一个结论:并发和并行都是对"多任务"处理的描述,其中并发是轮流处理,而并行是同时处理

CPU多核

现在的个人计算机动辄拥有十来个核心(M1 Max/Intel 12代)如果使用串行的方式那真是太低调了因此我们把各种任务简单分成多个队列每个队列都交给一个cpu核心去执行当某个cpu核心没有任务时它还能去其它核心的队列中偷任务(真·老黄牛),这样就实现了并行化处理。

单核心并发

那问题来了在早期只有一个CPU核心时我们的任务是怎么处理的呢其实聪明的读者应该已经想到是的并发解君愁。当然这里还得提到操作系统的多线程正是操作系统多线程 + CPU核心才实现了现代化的多任务操作系统。

在OS级别多线程负责管理我们的任务队列你可以简单认为一个线程管理着一个任务队列然后线程之间还能根据空闲度进行任务调度。我们的程序只会跟OS线程打交道并不关心CPU到底有多少个核心真正关心的只是OS当线程把任务交给CPU核心去执行时如果只有一个CPU核心那么它就只能同时处理一个任务。

相信大家都看出来了:CPU核心对应的是上图的咖啡机,而多个线程的任务队列就对应的多个排队的队列最终受限于CPU核心数, 每次只会有一个任务被处理。

和排队一样,假如某个任务执行时间过长,就会导致用户界面的假死(相信使用Windows的同学或多或少都碰到过假死的问题) 那么就需要CPU的任务调度了(真实CPU的调度很复杂我们这里做了简化),有一个调度器会按照某些条件从队列中选择任务进行执行,并且当一个任务执行时间过长时,会强行切换该任务到后台中(或者放入任务队列,真实情况很复杂!),去执行新的任务。

不断这样的快速任务切换对用户而言就实现了表面上的多任务同时处理但是实际上最终也只有一个CPU核心在不停的工作。

因此并发的关键在于:快速轮换处理不同的任务,给用户带来所有任务同时在运行的假象。

多核心并行

当CPU核心增多到N时,那么同一时间就能有N个任务被处理,那么我们的并行度就是N,相应的处理效率也变成了单核心的N倍(实际情况并没有这么高)。

多核心并发

当核心增多到N时,操作系统同时在进行的任务肯定远不止N个,而这些任务将被放入M个线程队列中,接着交给N个CPU核心去执行最后实现了M:N的处理模型,在这种情况下,并发跟并行时同时在发生的,所有用户任务从表面来看都在并发的运行,其实实际上,同一时刻只有N个任务能被同时并行的处理

看到这里,相信大家已经明白两者的区别,那么我们下面给出一个正式的定义(该定义摘选自<<并发的艺术>>)。

正式的定义

如果某个系统支持两个或者多个动作的同时存在,那么这个系统就是一个并发系统。如果某个系统支持两个或者多个动作同时执行,那么这个系统就是一个并行系统。并发系统与并行系统这两个定义之间的关键差异在于 “存在” 这个词。

在并发程序中可以同时拥有两个或者多个线程。这意味着,如果程序在单核处理器上运行,那么这两个线程将交替地换入或者换出内存。这些线程是 同时“存在” 的——每个线程都处于执行过程中的某个状态。如果程序能够并行执行,那么就一定是运行在多核处理器上。此时,程序中的每个线程都将分配到一个独立的处理器核上,因此可以同时运行。

相信你已经能够得出结论——“并行”概念是“并发”概念的一个子集。也就是说,你可以编写一个拥有多个线程或者进程的并发程序,但如果没有多核处理器来执行这个程序,那么就不能以并行方式来运行代码。因此,凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

编程语言的并发模型

如果大家学过其它语言的多线程,可能就知道不同语言对于线程的实现可能大相径庭:

  • 由于操作系统提供了创建线程的API因此部分语言会直接调用该API来创建线程因此最终程序内的线程数和该程序占用的操作系统线程数相等一般称之为1:1线程模型例如Rust
  • 还有些语言在内部实现了自己的线程模型(绿色线程、协程)程序内部的M个线程最后会以某种映射方式使用N个操作系统线程去运行因此称之为M:N线程模型, 其中M和N并没有特定的彼此限制关系。一个典型的代表就是Go语言
  • 还有些语言使用了Actor模型基于消息传递进行并发, 例如Erlang语言

总之每一种模型都有其优缺点及选择上的权衡而Rust在设计时考虑的权衡就是运行时(runtime)。出于Rust的系统级使用场景且要保证调用C时的极致性能它最终选择了尽量小的运行时实现。

运行时是那些会被打包到所有程序可执行文件中的Rust代码根据每个语言的设计权衡运行时虽然有大有小(例如Go语言由于实现了协程和GC运行时相对就会更大一些),但是除了汇编之外,每个语言都拥有它。小运行时的其中一个好处在于最终编译出的可执行文件会相对较小,同时也让该语言更容易被其它语言引入使用。

而绿色线程/协程的实现会显著增大运行时的大小因此Rust只在标准库中提供了1:1的线程模型,如果你愿意牺牲一些性能来换取更精确的线程控制以及更小的线程上下文切换成本,那么可以选择Rust中的M:N模型,这些模型由三方库提供了实现,例如大名鼎鼎的tokio

在了解了并发和并行后我们可以正式开始Rust的多线程之旅。