11.9 小结

本章的目标是给读者提供一个采用线程进行并发编程的基础:

1)可以运行多个独立的任务。

2)当这些任务关闭时,必须全面地考虑所有可能的问题。在任务完成之前,它们使用的对象或其他任务可能会消失。

3)任务在彼此争夺共享资源时会产生冲突。互斥锁是用来防止这些冲突的基本工具。

4)如果不谨慎设计的话,任务可能死锁。

然而,有很多其他有关线程处理方面的工具,可以帮助来解决线程处理的问题。ZThread库就包含有很多这样的工具,比如,信号量(semaphore)和在本章中所见到的与队列相似的特殊类型的队列。可以探究这个库以及其他有关线程处理的专题资源来得到更深入的知识。

至关重要的是要学会什么时候应该使用并发,以及什么时候应该避免使用并发。使用它的主要原因是:

·为了处理许多任务,这些任务交织在一起,应用并发可以更有效地使用计算机(包括透明地分配任务到多CPU的能力)。

·为了能够较好地组织代码。

·为了用户使用更方便。

资源均衡的经典例子是在I/O等待期间使用CPU。给用户带来便利的经典例子是在长时间下载过程期间监视“停止”按钮是否按下。

线程额外的优点是它们提供“轻量级”的执行语境切换(约为100条指令)而非“重量级”进程语境切换(约上千条指令)。由于一个给定的进程中所有的线程共享同一内存空间,一个轻量级语境切换只改变了程序执行的先后顺序和局部变量。进程改变—重量级语境切换—必须调换所有内存空间。

多线程处理的主要缺陷是:

·当等待共享资源时性能降低。

·处理线程需要额外的CPU开销。

·拙劣的程序设计决定会引发毫无益处的复杂性。

·为诸如饥饿、竞争、死锁和活锁等病态行为的出现创造了机会。

·跨平台操作造成的不一致性。在为本章开发原始材料(使用Java)时,作者就发现竞争条件在某些计算机上会很快出现,但在另外的计算机上则不会。本章中的C++例子在不同的操作系统下其行为是不同的(但通常是可接受的)。如果在某台计算机上开发一个程序,并且工作似乎一切正常,然而当发布它时你也许会因得到完全不受欢迎的结果而大吃一惊。与线程一起存在的最大的困难之一是,因为多个线程也许在共享某个资源—比如,一个对象

中的内存—并且还要必须确定多个线程不会在同时读取和改变那个资源。这需要头脑精明地使用同步工具,必须要彻底理解这些同步工具,因为它们可以神不知鬼不觉地将程序引入到死锁的境遇。

另外,线程的应用有一定的技巧。C++被设计为允许创建足够多的对象来满足解决问题的需要—至少在理论上是这样。(比如:为进行工程上的有限元分析而创建上百万个对象,这可能是不现实的。)然而,想要创建的线程数目通常有一个上限,因为在达到某个数量时,线程的性能就会变得极差。这个临界点很难探测,且常常依赖于操作系统和线程库;它可以是少于一百个,也可能达到数千个线程。就我们常常只创建少量的线程以解决某个问题而言,这个限制没有多大作用;但是在更一般的设计中它就会变成一个约束。

用一种特定的语言或库来进行线程处理不管似乎有多么简单,都认为它是一种变幻无常的魔术。人们在编程时总会有些没有考虑周全的地方,因此就会在你最没有预料到的时候“咬你一口”。(比如,请注意因为哲学家进餐问题可以进行调整,所以死锁很少发生,人们就会得到一切都万事大吉的假象。)这里引用Python编程语言的发明者Guido van Rossum的一个恰当的描述:

在任何多线程处理的程序设计中,大多数的错误来自于线程处理问题。这和编程语言无关—它是深层次的问题,即人们至今仍未完全理解的线程的性质。

有关线程处理更高级的讨论,请参看《Parallel and Distributed Programming Using C++》一书,Cameron Hughes和Tracey Hughes著,Addison-Wesley出版社2004年出版。