第11章 并发

对象提供了将一个程序分解为若干个独立部分的途径。在实际的工作中也经常需要把一个程序分割成若干个分开的、独立运行的子任务。

使用多线程处理(multithreading),每个独立的子任务都会被执行的线程(thread of execution)驱动,程序就好像每个线程都拥有自己的CPU。其底层实现机制实际上为线程划分出了CPU时间,但是在一般情况下,程序员在编程时并不需要去考虑它,这有助于简化多线程编程。

进程(process)是在其自己的地址空间运行的自含式(self-contained)程序。周期性地把CPU从一个任务切换到另一个任务,多任务处理(multitasking)操作系统在同一时刻可以运行多个进程(程序),使得它们看上去就好像都在独自运行。线程(thread)是一个进程内的单一连续的控制流。因此一个进程可以有多个并发执行的线程。由于这些线程运行在一个进程内,所以它们分享内存和其他资源。编写多线程处理程序中主要的困难就是在不同线程之间协调对这些资源的使用。

多线程处理有多种应用,而当程序的某些部分与一个特定事件或资源结合在一起的时候,最经常需要使用多线程。为了防止挂起程序的其余部分,需要创建一个与那个特定事件或资源关联在一起的线程,并使这个线程独立于主程序运行。

学习并发编程像是步入了一个崭新的世界,类似学习一门新的编程语言,或至少是学习一组新的语言概念。随着在大多数的微机操作系统中出现了支持线程的操作,在编程语言或者程序库中,也出现了用于线程的功能扩充。总而言之,线程编程:

1)不仅看起来神秘,而且需要人们转换一下思考编程的方式。

2)各种语言中对线程的支持看上去都是相似的。当理解了线程时,就会理解一个共同的表述方式。

理解并发编程与理解多态性有类似的难度。经过一番努力,就可以彻底了解其基本机制,但一般需要深入地学习和理解才能够真正掌握其实质。本章的目标是给读者打下有关并发编程基本原理的坚实基础,这样就会理解基本概念并且编写出合理的多线程处理程序。不过读者也要意识到,这也许会使你很容易变得太过自信。如果要编写任何复杂的程序,则需要研读关于这个主题的专著。

11.1 动机

使用并发的最能激发人们兴趣的理由之一,就是产生一个可做出响应的用户界面。考虑一个程序,其在执行某项强烈需要CPU的操作时,往往会忽略用户的输入并且无法做出响应。程序既需要继续执行其操作,又需要把控制权归还给用户界面,这样程序才可以响应用户的请求,这就是问题的关键。如果有一个“退出”按钮,我们不希望被迫在程序中的每个代码块中轮循检测它的状态。(这将会使数个退出按钮代码贯穿整个程序,对它的维护很让人头痛。)然而,却希望对这些退出按钮能够做出响应,就好像系统在定期地检测它一样。

传统的函数不可能在继续进行其操作的同时,又把控制权归还给程序的其余部分。事实上,这听起来像是一个不可能完成的任务,就好像一个CPU必须能同时出现在两个地方,但这正是严谨的并发机制提供的错觉效果(在多处理器系统的情形中,这可不只是错觉)。

也可以使用并发机制来优化信息的吞吐量。比如,程序在等待信息输入到达I/O端口的时候可以做些其他重要的工作。要是没有线程处理,惟一可行的解决方法就是不断轮询I/O端口,但这个方法不仅笨拙而且实现起来比较困难。

如果有一台多处理器的计算机(multiprocessor machine),多个线程就可以分布在多个处理器上,用此方法可以极大地提高信息的吞吐量。这种情况通常出现在使用功能强大的多处理器的web服务器上,这样一来,就可以在程序中给每个请求分配一个线程,将大量的用户请求分配到多个CPU来进行处理。

在单CPU计算机上,一个使用多线程的程序仍然一次只能做一件事情,所以不使用任何线程编写出具有相同功能的程序在理论上是可能的。然而,多线程处理提供的重要好处是在程序的组织方面,可以使程序的设计极大的简化。某些类型的问题,比如模拟—例如,一个视频游戏—如果不支持并发是很难解决的。

线程处理模型为编程方式提供了方便,可以在同一时间内魔术般地简化一个程序中的多个操作:CPU将会轮流给每个线程分配一些CPU时间。[1]每个线程都觉得自己在一直占有CPU,但事实上CPU时间被切成片段分配给所有的线程。运行在多CPU计算机上的程序是个例外。但是,关于线程处理的一个重大好处是可以使人们从这一层次中抽出身来,所以代码不需要知道实际上是运行在单CPU计算机上还是多CPU计算机上。[2]因此,使用线程是创建透明可扩展程序的一条途径—如果一个程序运行得太慢,可以很容易地给所使用的计算机增加CPU来加速程序的运行。现在的趋向是,进行多任务处理和多线程处理是利用多处理器系统最合理的途径。

线程处理多少会降低进行计算的效率,但是从改善程序设计、资源平衡以及给用户提供方便等方面来说,还是相当值得的。一般情况下,使用线程能够创建一个更加松散耦合的设计(loosely coupled design);否则,部分代码将被迫对这些通常由线程处理的工作花费更大的精力。

[1]当系统使用时间分片机制时(比如Windows)这是正确的。Solaris使用一个FIFO并发模型:除非一个更高优先级的线程被唤醒,当前的线程会一直运行直到它被阻塞或终止。那意味着其他有相同优先级的线程直到当前线程放弃处理器后才会运行。

[2]假设我们为多CPU设计了它。否则在一个时间分片的单处理器系统上似乎运行良好的代码在移植到多CPU系统上时会失败,这是由于额外的CPU会引发问题而单CPU系统则不会。