CH01-概述

基于摩尔定律的“免费午餐”时代已结束,为了让代码运行的更快,现在需要以软件的形式利用多核优势,发掘并行编程的潜力。

并发 vs. 并行

并发程序含有多个逻辑上的独立执行块,它们可以独立的并行执行,也可以串行执行。

并行程序解决问题的速度往往比串行程序快的多,因为它可以同时执行整个任务的多个部分。并行程序可能有多个独立执行块,也可能仅有一个。

并发是问题域中的概念——程序需要被设计成能够处理多个同时(或者几乎同时)发生的事件;而并行则是方法域中的概念——通过将问题中的多个部分并行执行来加速解决问题。

来自 Rob Pike 的经典描述:

  • 并发是同一时间应对(dealing with)多件事情的能力。
  • 并行是同一时间动手做(doing)多件事情的能力。

并发与并行经常被混淆的原因之一是,传统的“线程与锁”模型并没有显式的支持并行。如果要用线程与锁模型为多核进行开发,唯一的选择就是写一个并发的程序,然后并行的运行在多核上。

并发程序的执行通常是不确定的,它会随着事件时序的改变而给出不同的结果。对于真正的并发程序,不确定性是与生俱来且伴随始终的属性。与之相反,并行程序可能是确定的——比如将数组中的每个数都加倍,一种做法是将数组分为两个部分然后分别交给两个核处理,这种做法的结果是确定的。

并行架构

人们通常认为并行等同于多核,但现代计算机在不同层次上使用了并行技术。比如在由多个晶体管组成的单个核内,可以在位级和指令级两个层次上并行使用这些晶体管资源。

位级并行

因为并行,32 位计算机要比 8 位计算机的运算速度快。对于两个 32 位数的加法运算,8 位计算机必须进行多次 8 位运算,而 32 位计算机可以一步完成。计算机经历了 8、16、32 位时代,目前处于 64 位时代,由于位升级带来的性能提升存在瓶颈,因此我们短期内无法进入 128 位时代。

指令级并行

现代 CPU 的并行度很高,其中使用的技术包括流水线、乱序执行和分支预测等。

开发者可以不用关心 CPU 的内部并行细节,因为尽管 CPU 内部的并行度很高,但是经过精心设计,从外部看上去所有处理器都像是串行的。

但这种看上去像是串行的设计也逐渐变得不再适用。CPU 的设计者们为单核提升速度变得越来越困难。进入多核时代后我们必须要面对的情况是:无论表面上还是实质上,指令都不再是串行执行了。

数据级

数据级并行(也称单指令多数据,SIMD)架构,可以并行的在大量数据上施加同一操作。这并不适合解决所有问题,但在有些场景可以大展身手。

比如图像处理,为了增加图片亮度就需要增加每一个像素的亮度,现代 GPU 也因图像处理的特点演化成了极其强大的数据并行处理器。

任务级

这也是大家所认为的并行形式——多处理器。从开发者的角度看,多处理器架构最明显的分类特征是其内存模型(共享内存模型或分布式内存模型)。

  • 对于共享内存模型,每个处理器都能访问整个内存,处理器之间的通信也通过内存完成。
  • 对于分布式内存模型,每个处理器都拥有自己的内存,处理器之间的通信主要通过网络完成。
NAME

通过内存通信比通过网络通信更加简单快速,因此使用共享内存模型编写也更容易。但是当处理器个数不断增加,共享内存模型就会遇到瓶颈——这时不得不转向分布式内存模型。如果要开发一个容错系统,就要使用多台计算机以规避硬件故障对系统的影响,此时也必须借助分布式内存模型。

并发,不只是多核

并发的目的不仅仅在于让程序以并行方式运行以发挥多核优势。如果能够正确的使用并发,程序还能获得以下优点:及时响应、高效、容错、简单。

并发的真实世界

世界是并发的,为了与其有效交互,软件也应该是并发的。并发是系统及时响应的关键。比如,文件下载可以在后台运行,用户就不必等待鼠标上的沙漏了。再比如,Web 服务器可以并发的处理多个连接请求,一个慢请求不会影响服务器对其他请求的响应。

分布式世界

我们有时候需要解决地理分布问题。软件在非同步运行的多台计算机上分布式的运行,其本质也是并发。

此外,分布式软件还具有容错性。其中一台或一个区域内的几台机器宕机后,剩余机器仍然能够提供服务。

不可预测的世界

任何软件都无法避免 BUG 的存在,即便是没有 BUG,也无法完全避免硬件故障。为了增强软件的容错性,并发代码的关键是独立性和故障检测。

  • 独立性:一个故障不会影响到故障任务以外的其他任务。
  • 故障检测:当一个任务失败时,需要通知负责处理故障的其他任务来解决。

而串行程序的容错性远不如并发程序。

复杂的世界

在选对编程语言和工具的情况下,一个并发的解决方案要比串行方案简单清晰。

用串行方案解决一个现实世界的并发问题往往需要付出额外的代价,而且最终方案会晦涩难懂。如果解决方案有着与现实问题类似的并发结构,事情就会变得简单很多。

七种模型

  • 线程与锁:存在很多总所周知的不足,但它是其他模型的基础,也是很多并发软件的首选。
  • 函数式编程:函数式编程逐渐变得重要的原因之一,是其对并发和并行编程提供了良好的支持。函数式编程消除了可变状态,所以从根本上就是线程安全的,而且易于并行执行。
  • 分离标识与状态:Clojure 是一种指令式与函数式混搭的语言,在两种编码方式上取得了微秒的平衡来发挥两者的优势。
  • Actor:是一种适用性很广的并发编程模型,适用于共享内存模型和分布式内存模型,也适合解决地理分布问题,能提供强大的容错性。
  • 通讯顺序进程(CSP):CSP 与 Actor 模型在表面上很相似,两者都基于消息传递。但是 CSP 模型侧重于传递信息的通道,而 Actor 模型侧重于通道两端的实体,因此基于 CSP 模型的代码会有明显不同的风格。
  • 数据级并行:GPU 利用了数据级并行,不仅可以快速的处理图像,还可以用于更加广阔的领域。
  • Lambda 架构:Lambda 架构综合了 MapReduce 和流式处理的特点,是一种可以处理多种大数据问题的架构。

后续将针对每种模型详细讨论以下问题:

  1. 该模型适用于解决并发问题?还是并行问题?还是两者都适用?
  2. 该模型适用于哪种并行架构?
  3. 该模型是否有利于写出高容错的代码,或是能够解决分布式问题的代码?