计算机并发和并行的体系

  • 并发(Concurrency):如果逻辑流在时间上重叠,那么它们就是并发的
  • 并行(Parallel):如果逻辑流在时刻上重叠,那么它们就是并行的

erlang 之父用一张图来描述了并发和并行的区别。

Concurrent&Parallel
Concurrent&Parallel

一些有 GIL 的语言,并不能实现并行,可以参照薄荷 CTO 的 Ruby 实例说明 Ruby 多线程的潜力和弱点。实际上,按照上述图片的例子,咖啡机就像是 CPU 的核心,部分语言即使在有多台咖啡机的情况下,任何时刻也只有一台咖啡机在工作。现代的互联网产品大多是 IO 密集型,而 IO BLOCK 这种操作严格意义上是不占用 CPU 时间的,这里可以参 Java 控制 CPU 占用 一文。所以,并行是并发的子集,通常情况下,我们研究的是并发。

进程的并发

简单的说,当一个程序被客户端请求的时候,就创建一个新的进程来服务客户端,进程对 CPU 资源的获取由内核调度。详细的就是:

  • 父进程监听着一个文件描述符 fd_a,当收到客户端请求的时候,立即打开并返回一个描述符 fd_b,
  • 父进程派生一个子进程,子进程获得一份父进程监听的文件描述符列表的副本,
  • 子进程关闭 fd_a,父进程关闭 fd_b(不关闭可能引起内存泄漏)
  • 子进程为客户端服务

多进程的并发可以实现对 CPU 多核心的充分利用,同时由于多进程之间各自维护自己的虚拟空间,这也不容易产生并发安全问题,erlang 的 Actor 模型就是成功的案例。坏处是如果这个程序含有多个进程,同时这些进程之间有通信的需求,需要一些外部的进程通信机制。

线程和协程

当今许多语言都有这自身独特的线程模型。在操作系统原生的设计中,线程是进程的实体,具有以下特点

  • 同一个进程中的线程共享了同一块虚拟内存
  • 线程由内核调度,所以
  • 内核可以让线程尽可能享用 CPU 资源
  • 多个线程一定是在竞争 CPU 资源
  • 当某个线程发生阻塞的时候,内核将 CPU 资源分配给别的线程,这里涉及到了上下文切换,与模式切换不同,

实际上

“操作系统的进程切换和 CPU 的模式切换并没有什么关系,发生模式切换可以不改变正处于运行态的进程状态,这种情况下,保存上下文环境和以后恢复上下文环境只需要很少的开销。但是,如果改变正处于运行态的进程状态到另一个状态(就绪、阻塞等),则操作系统必须使其环境产生实质性的变化”——《操作系统:精髓与设计原理》

有时候,编程语言中,CPU 资源的分配用户希望把控制权从内核转交给自身,这时候,另一个概念 – 协程便产生了。通常情况下,多个协程对应着一个操作系统线程。协程的存在让用户可以自己管理程序中不同逻辑流的 CPU 分配,然而,当对应的这个操作系统线程阻塞的时候,所有的协程就可能一起阻塞;这时候就需要语言有对应的调度器来控制。现代语言中的 Golang 就做得很好。

IO 多路复用

还是由于现在的互联网产品大多数 IO 密集型,所以很多场景上,单个线程 / 进程就可以实现并发。实际上,操作系统已经做到了这一点,在多个客户端都连接上同一个服务器的时候,这个服务器同时打开了对应个数 + 的 fd,服务端要做的就是当某个或者某些连接被客户端传过来的时候,能够立即处理并且传递给 fd。类似于

1
2
3
for x in open_connections:
if has_new_input(x):
process_input(x)

这样做的坏处是会浪费大量的 CPU 时间。应用程序只关心发生变化的 fd,比如:某个 df 变为可以读或者可写。而这个任务现在只需要交给操作系统,在 linux 中,select、poll 和 epoll 都是实现 IO 多路服用的 system call。它们的主要作用就是

  • 应用程序把一堆文件描述符传入,然后
  • 等待返回告诉哪些文件描述符是可读 / 可写

Julia Evans 的 Async IO on Linux: select, poll, and epoll 比较详细说明了上述三种方法的工作方式

并发安全

我认为,并发安全可以用一个最简单的例子解释。

有 N 个任务都会对数据 a 进行 write(write 的方式需要取决于 a 的值),如果这 N 个任务并行执行完 a 的值和串行执行完不一致,那么这个并发就是不安全的。这个概念和 数据竞态 类似。可以例举一些有并发安全问题的场景

  • 数据库中某 column x 在业务中 unique,程序员采用 find x or insert
  • n++ 问题
  • 火车售票问题

总结下来,当存在 n = f(n - 1) 这样的场景时,很有可能出现并发安全问题