并发(Concurrent)和并行(Parallel)

在 Rust 中由于语言设计理念、安全、性能的多方面考虑,而是选择了多线程与 async/await 相结合。
优点是可控性更强、性能更高,缺点是复杂度并不低,当然这也是系统级语言的应有选择:使用复杂度换取可控性和性能

由于一个 CPU 核心在同一时刻只能执行一个任务,为了避免 IO 等阻塞操作占用 CPU,需要用并发和并行提高 CPU 利用率。并发和并行是两个概念,不完全相等。

  • 并发(Concurrent) 是指一个 CPU 核心在一个极小的时间片段内,快速轮换处理多个任务的部分计算。即多个任务被轮换处理,一次处理一个任务的一部分计算。
  • 并行(Parallel)是指多个 CPU 核心同时执行多个任务,一次可以执行多个任务。

Erlang 之父对于 Concurrent(并发) 和 Parallel(并行) 的解释:

  • 单线程:一个咖啡机, 有多于两个队伍的人都需要使用这一个咖啡机接咖啡, 调度员只在一个队伍的人接到咖啡之后, 才允许下一个队伍使用这台咖啡机
  • 单线程并发:仍然只有一个咖啡机, 队伍数量不变, 调度员这次取消了一个队伍的人全部接完咖啡后才能切换队伍的限制, 允许这次 A 队的人接, 下次 B 队的人接, 总体上队伍之间的进展比较协调
  • 并行:有多个咖啡机,多个队伍可以同时接咖啡
  • 1:1 并行:有几个队伍, 就有几个咖啡机, 队伍之间互不干扰
  • M:N 并行:M 个队伍, N 个咖啡机 (M > N), 调度员可以根据情况, 选择让某几个队伍, 在某个咖啡机上, 形成单线程并发

并发和并行都是对“多任务”处理的描述,其中并发是指多个任务被快速轮换处理,而并行是多个任务被同时处理。

并发和并行总共可以分为三类:单核心并发,多核心并行,多核心并发。

单核心并发

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

假如某个任务执行时间过长,就可能导致用户界面的假死。此时就需要 CPU 的任务调度器,它会按照某些条件从任务队列中选择任务进行执行,并且当一个任务执行时间过长时,调度器会强行切换该任务到后台中,去执行新的任务。
不断这样的快速任务切换,对用户而言就实现了表面上的多任务同时处理,但是实际上最终也只有一个 CPU 核心在不停的工作。

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

多核心并行

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

多核心并发

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

<<并发的艺术>> 的正式定义

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

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

并发中中的 “同时” 指的是 “多个任务同时存在”,在一个极小的时间片段内,多个任务轮换执行。而并行中的同时强调的是 “多个任务同时执行”,在一个时刻同时执行多个任务。
同一个时间片段内并不代表同时执行,但是一个时刻同时执行多个任务,那么在这个时刻周围的一段时间必然同时存在多个任务。

也就是说:“并行”概念是“并发”概念的一个子集,一个多线程/进程程序,如果没有多核处理器执行,那就不能以并行的方式运行代码。
凡是在求解单个问题时涉及多个执行流程的编程模式或者执行行为,都属于并发编程的范畴。

编程语言的并发模型

不同的编程语言对于线程的实现可能大相径庭,但最终都是通过操作系统的线程来运行:

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