使用面向对象组件构建多线程应用程序时,程序员必须考虑的重要两点是:
a. 构建应用程序需要的组件;
b. 这些组件在多线程环境中的行为。
1. 线程、对象和作用域
对象具有如下4种作用域类型:
a. 局部作用域(local scope); b. 函数作用域(function scope); c. 文件作用域(file scope); d. 类作用域(class scope)。
1.1 连接与作用域
一个线程可以访问另一个线程的堆栈片断。当一个程序创建多个线程时,每个线程都可以访问创建它们的进程的数据片断、堆栈片断、代码片断以及堆。当多个线程访问一个对象时,作用域、对象以及对象的连接决定了线程是否可以直接访问这个对象;或者这个对象是否必须通过参数从一个线程传递到另一个线程。
当文件作用域对象具有外部连接时,它可以被创建该对象的进程中的任何线程自由访问,不需要传递参数。文件作用域对象和外部连接对于整个进程及其所有线程是全局性的。另一方面,文件作用域对象和内部连接可以被同一翻译单元的函数部分自由访问,不必进行参数传递。不过,为了被其它翻译单元中的函数访问,该对象必须作为一个参数进行传递。就像程序可以执行在多个翻译单元中定义的函数和过程一样,线程也可以执行在多个翻译单元中定义的函数和过程。
如果线程在执行一个函数,这个函数就容易访问位于函数局部翻译单元内的对象以及具有文件作用域和外部连接的对象。为了让线程的函数访问具有文件作用域和内部连接的对象,必须通过参数将对象传递给线程的函数。
至少有一种方法可以从进程内的任一个线程访问任一个对象。这种访问可以是单向的,即表示接收线程只能读对象,而不能更改它。这就是通过值传递对象的情况,这与通过引用传递相反。同样,如果将对象声明为const,情况也是这样。如果对象通过引用或指针来传递,则对象充当线程间的通信链接,而且线程间的通信是双向通信。这意味着线程A对象所作的任何个性都立即影响线程B,同时线程B所做的修改也立即影响到线程A。
1.2 线程和类作用域
设想在一个对象的成员函数内创建了一个线程,此时成员函数和线程并没有父-子关系。由成员函数传递的线程在访问对象的内部数据成员上没有任何特殊优先权。一旦创建了线程,对象访问就由对象作用域和连接所控制。
2. 同步关系和对象成员函数
在一个进程内,对象与多个线程可能具有4种基本同步关系:
a. 对象被需要同步的多个线程访问; b. 对象被分解成需要同步的多个线程; c. 对象被分解成需要同步的多个线程,同时,对象也能被需要同步的多个线程访问; d. 对象只能被单个线程访问,而且不能分解成多个线程。
当对象创建的线程试图并发修改对象的临界区时发生通信依赖性关系。为了防止线程破坏对象的临界区,这些线程间通过互斥量来通信。当多个线程为共同的目标而工作,每个线程解决问题的一部分,此时就发生同步依赖性关系。每个部分的执行必须按正确的序列进行,以便对象从整体上协调完成它的工作。同步化对象的线程所执行的任务,线程将使用条件变量、等待函数或事件互斥量。
3. 在多线程环境中构建和析构对象
3.1 exit()和abort()
当使用exit()时,系统尝试清空缓冲器、释放资源,并在可能的地方调用析构函数。如果析构函数中存在锁定或释放机制,则调用exit()通常会让挂起的析构函数执行。
当使用abort()调用时,所有执行都被取消。如果存在还没有调用的析构函数,执行abort()请求后它们不被调用。如果这些析构函数包含一些当前占有互斥量的取消锁定或释放机制,则应用程序将退出并保持对互斥量的占有权,其它所有等待该互斥量的进程或线程可能被无限延迟。在多线程环境中强烈推荐使用C++异常处理机制,在程序失败或抛出异常时,结构化异常处理让程序员有机会决定通过同步机制如何应付。
3.2 构造函数和SS关系
在同一个进程中创建多个线程时,它们可能并发执行、同时执行,也可能异步执行。
两个线程并发执行时,它们在同一时间段内执行,不一定在同一时刻执行;
多个线程同时执行时,则是指它们在同一时刻执行。只有多处理器系统可以让某进程中的多个线程在同一时刻执行;
多个线程异步执行时,它们可能并发执行、同时执行,也可能会依次执行。对于异步执行,不能保证多个线程的执行方式。在异步执行的进程中,线程执行的顺序得不到保证。
3.3 析构函数与FF关系
3.4 线程集合与对象
在使用接口类封装操作系统API时,重要的优势是封装(encapsulation)和保护(protection)。
在多线程环境中,保护互斥量或条件变量不漂浮不定是消除导致竞争条件、死锁以及无限延迟情况发生所必需的。我们还可以使用封装来控制线程取消,即通过另一个线程的调用终止一个线程的执行。
取消线程可以有效停止它的跟踪和销毁。
由于某些原因,一个线程被锁定,而且不能对任何通信尝试作出反应,这样的线程必须取消。陷入致命或无限循环的线程必须取消。不过,在大部分情况下,应当使用条件变量和事件互斥量来控制其活动不再需要的线程。防止杂乱线程取消的第一步是在某个对象中封装线程句柄。它只能被类的成员函数访问。唯一取消这个对象占有的线程的途径是通过cancel成员函数。如果线程对象被它的宿主对象私有性地占有,则只有宿主对象才能取消此线程。而如果线程的句柄为全局变量,或者可以被多个线程访问,它可能被错误地取消。随着在应用程序中添加更多的模块和线程,这种犯错的可能性也增加。
3.5 线程与异常处理
异常处理所隐含的基本思想是让不能处理特定问题的组件将该问题传递给另一个知道如何解决此问题的组件。使用异常处理,我们可以设计特殊目的的组件,它的特定功能就是应付错误(在发生错误的情况下),并且处理其它软件异常。
C++异常处理机制提供的三种重要功能:
a. 允许没有返回值的函数抛出异常对象;
b. 允许函数将抛出结构用作替换返回机制来返回多种类型返回值;
c. 允许程序员以异常对象的形式定义情景敏感性诊断(situation sensitive diagnostics)。这些异常对象可以让异常情景更容易理解、维护和调试。
异常的抛出和捕获是通过调用堆栈的协助来完成的。进程中的每个线程都有自己的调用堆栈,因此不要试图从一个线程到另一个线程抛出异常。
可以借助于异常处理机制来识别和处理死锁。抛出异常的进程可以包含标识死锁情形的逻辑。抛出对象可以包含针对每个侵犯线程的线程对象。处理器然后决定做什么。处理器可以强迫从两个线程移除资源(如果可能的话)。处理器可以决定取消两个线程,从整体上保持系统的执行。处理器处理完死锁后,处理器可以执行一些可选路径,这是为碰到死锁后预备的。使用异常机制和用户自定义,异常类可以提高多线程应用程序顺利执行而不崩溃的成功率。
4. 线程安全函数
当函数在某一时刻被多个线程调用,而且不要求任何施加于调用者部分的动作时,函数或函数集被认为是线程安全的或可以重复进入的。对于某些或包含静态变量、访问全局数据,或不能重复进入的函数就是不安全的。当前大部分编译器提供了标准库的多线程版本。在可能应用于多线程环境的时候,程序员应当连接标准标准库的多线程版本。
5. 多线程环境中的不安全函数
如果不知道来自库的哪一个函数是安全的,哪一个是不安全的,程序员就有3种选择:
a. 不要在应用程序中使用任何不安全函数;
b. 限制在单线程中使用所有不安全函数;
c. 通过一套同步机制封装所有的可能不安全函数。
6. 在多线程架构中使用STL算法
为了使用STL我们可选择牺牲灵活性换取线程安全。我们使用接口类来解决问题,通过复合结合容器类与接口类,而且使用成员函数封装STL算法来达到线程安全的标准。