6.1 并发编程基础

并发这个概念由来已久。其主要思想是使多个任务可以在同一时间执行以便能够更快地得到结果。几乎在计算机问世的时候,并发就已经出现在它的世界中了。

并发编程的思想来自于多元程序(multiprogramming,或称多任务)操作系统。多元程序操作系统允许同时运行多个程序。在早期的单用户计算机系统的操作系统中,任务是被一个接一个地读取、寻找资源并运行的。各个任务的执行完全是串行的,只有在一个任务被运行完成之后另一个任务才会被读取。而多元程序操作系统则允许终端用户同时运行多个程序。当一个程序暂时不需要使用CPU的时候,系统会把该程序挂起或中断,以使其他程序可以使用CPU。

最早支持并发编程的计算机编程语言是汇编语言。不过那时并没有任何理论基础来支持这种编程方式。这使得一个细微的编程错误就会使程序变得非常不稳定。并且,对这种程序的测试也是几乎不可能的。

在20世纪60年代末,多元程序操作系统已变得非常臃肿和脆弱。对系统资源的无限制地抢夺成为了频繁发生的程序死锁现象的导火索。连其缔造者都不得不公开发表言论说,这是一场软件危机。

不过,早在20世纪60年代中叶,计算机科学家就迈出了深入探索并发编程的第一步。在不到15年的时间里,他们逐步把并发编程思想凝炼成了理论,同时开发出了一套关于它的描述方法。之后,他们把这套理论融入到了编程语言当中,并用这些编程语言来编写操作系统模型。在20世纪70年代,第一本关于操作系统和并发编程原则的简明参考书问世了。这意味着一个新的编程理论正式形成。正如上面介绍的那样,发展并发编程思想的最初动机来源于开发可靠的操作系统的强烈欲望。但在并发编程理论形成不久之后,它就被公认为一个可以不仅被应用于操作系统开发的通用编程理论。

经过多年的演进和变化,并发程序的编写早已没有先前那么复杂了。大多数现代软件设计技术都可以很方便地产生出支持并发的程序。在它们看来,只有一个例程(routine)的并发程序其实就相当于一个串行程序。这种转换是平滑的和透明的。总体来看,编程人员感觉编写并发程序会更加困难的原因有两个。

  • 缺乏既非常适合开发应用程序又对并发编程有良好支持的编程语言。

  • 感觉(仅仅是感觉)并发编程的理论太难了。

第一个原因的确是存在的。不过,现在许多善于开发应用程序的编程语言都在致力于降低用它们编写并发程序的门槛。至于第二个原因,我认为这不应该成为专业编程人员躲避编写并发程序的借口。况且,由于越来越多的编程语言对并发编程具有良好的支持,我们在学习计算机编程的时候,更加不可避免地接触到并发编程。毫不夸张地讲,如果我们想要真正理解一门编程语言以及了解怎样才能编好程序,那么学习并发编程这一步也是必不可少的。更不用说,作为软件运行的基础——计算机硬件也越来越向着并行化发展。

在本章中,我们会对当今主流的一些并发编程模型进行阐述和对比。在此基础上,我们会专门针对Go语言的并发编程模型进行详细的说明。不过在这之前,让我们先来简要介绍一下并发编程理论中的一些基础概念。

6.1.1 串行程序与并发程序

一个串行程序特指一个只能被顺序执行的指令列表。而一个并发程序则是被并发的执行的两个或两个以上的串行程序的统称。并发程序允许其中的串行程序运行在一个或多个可共享的CPU之上,同时也允许每个串行程序都运行在专为它服务的CPU上。前一种方法也被称为多元程序。多元程序由来自荷兰的图灵奖得主Edsger Wybe Dijkstra在1968年提出。多元程序由操作系统内核支持并提供多个串行程序复用多个CPU的方法。这种方法被称为多元处理。多元处理是指计算机中的多个CPU共用一个存储器(即内存),并且在同一时刻可能会有数个串行程序分别运行在不同的CPU之上。它是由美国计算机科学家Anita K. Jones和Peter M. Schwarz在1980年提出的。无论从理论还是实践角度看,多元程序和多元处理都是串行程序得以并发甚至并行运行的基础支撑。在现代计算机系统中,这两种方式已经得到了很好的融合。

6.1.2 并发程序与并行程序

在一些参考文献和图书中,常常把并发和并行这两个概念混淆在一起。但是,它们实际上有着很清晰的区别。并发程序是指可以被同时发起执行的程序。而并行程序则是被设计成可以在并行的硬件上执行的并发程序。换句话说,并发程序代表了所有可以实现真正的或者可能的并发行为的程序。它是一个比较宽泛的概念。这其中包含了并行程序。并行程序是并发程序中的一种。

6.1.3 并发程序与并发系统

首先,并发程序属于程序。即使它被划分为许多部分(可以是规模更小的程序),只要这些部分之间是紧密地关联在一起的,并且可以被看作一个概念上的整体,那么它就属于一个程序,也可以称之为一个内聚的软件单元。另一个方面,程序与程序之间可以通过协商一致的协议进行通讯,并且它们之间是松耦合的。它们可以被看作是一个系统,而不是程序。并发程序和并发系统中的并发的含义是一致的。但是,并发系统更有可能是并行的,因为其中的多个程序一般可以同时在不同的硬件环境上运行。因此,并发系统也常常被称为并行系统。与并发系统同义的一个更流行的词是分布式系统。

本章以及全书所关注的并发编程意在编写并发程序,而不是实现并发系统。

6.1.4 并发程序的不确定性

一个串行程序中的所有活动的先后顺序都是固定的,而一个并发程序中的活动只是部分有序的,也就是说其中一些活动的发生顺序并没有被明确地指定。这一特性被称为不确定性。这种不确定性导致了并发程序的每次运行的活动执行路径都是不同的,即便是在输入数据相同的前提下。

6.1.5 并发程序内部的交互

我们已经知道,并发程序内部会被划分为多个部分,每个部分都可以被看作是一个串行程序。在这些串行程序之间可能会存在交互的需求。比如,多个串行程序可能都要对一个共享的资源进行访问。又比如,它们需要相互传递一些数据。在这种情况下,我们就需要协调它们的执行。这就涉及了同步。同步的作用是避免在并发访问共享资源时可能存在的冲突,以及确保在互相传递数据时能够顺利地接通。我们会在本章后续部分详细地介绍各种同步的方法。

根据同步的原则,程序如果想使用一个共享资源,就必须先请求该资源并获取到对它的访问权。当程序不再需要某个资源的时候,它应该释放该资源,即放弃对它的访问权。一个程序对资源的请求不应该导致其他正在访问该资源的程序中断,而应该等到那个程序释放该资源之后再进行请求。也就是说,在同一时刻,某个资源应该只被一个程序占用。

传递数据是并发程序内部的另一种交互方式。它也被称为并发程序内部的通讯。实际上,协调这种内部通讯的方式不只同步这一种。我们也可以用异步的方式对通讯进行管理。这种方式使得数据可以不加延迟地发送给数据接收方。即使在数据接收方还没有立即为接收该数据做好准备的时候,也不会造成数据发送方的等待。数据会被临时存放在一个被称为通讯缓存的数据结构中。通讯缓存是一种特殊的共享资源,它可以同时被多个程序使用。数据接收方可以在准备就绪之后按照数据被存入通讯缓存的顺序从通讯缓存中接收它们。我们稍后还会看到对这两种交互方式的描述和对比。

好了,上面这些知识作为讲解并发编程模型的铺垫已经足够了。在这之后,我会对当今的主流并发编程技术进行介绍。