《C++面向对象高效编程(第2版)》——2.20 什么是多线程安全类

2.20 什么是多线程安全类

C++面向对象高效编程(第2版)
传统上,操作系统(OS)只支持进程(也称为任务)。每个进程都有自己的地址空间,且有一个单独的执行线程,进程执行一个包含一系列指令的程序。但是,现在大多数操作系统都支持单进程中的多线程。一个任务可根据需要包含多个线程,单进程中的所有线程共享进程的地址空间,线程可以访问进程中的所有全局数据。

使用线程有很多优点。首先,创建线程的成本更低(且更效率)。创建一个新进程需要涉及操作系统中的大量工作(设置内存页面、注册、进程上下文等),而线程需要的大多数资源都可以从进程中获得。其次,线程间的通信更加容易。不同的进程位于不同的地址空间中,因此进程之间的通信(IPC)并不容易。但是,在单个进程中,不同的线程共享相同的地址空间,因此线程之间的通信非常容易。另外,在多线程中,可以阻止(block)操作,也可以并行处理操作。操作系统单独调度(schedule)每一个线程。例如,有一个应用程序,允许从一个设备上复制文件到另一个设备上,用户可能要求在中途停止复制操作。如果创建一个仅用于等待用户输入的单独进程,显然很不合理。在这种情况下,创建一个等待用户输入的单独线程更加合适。主应用程序执行复制操作,而辅助线程等待用户输入。如果用户决定终止复制操作,则运行等待用户输入的线程,并通知主应用程序,用户要求中止操作;然后,主应用程序线程中止复制操作。在单个应用程序中使用多线程非常普遍。再举另外一例,文档处理应用程序可以用一个线程打印文档,而另一个线程执行生成索引,同时还有另一个线程用于接收用户提供的文档摘要信息。

任何使用多线程的应用程序都可称为多线程应用程序。涉及多个线程时,同步(synchronization)和互斥(mutual exclusion)尤为重要。当一个线程正在访问某段数据时(例如,打印文档的线程),必须防止其他线程试图访问相同的数据(为了写入)。根据操作系统,实现可以使用互斥体(mutex)、信号量(semaphore)、临界区(crical section)、自旋锁定(spin-lock)、消息、事件等来达到这个目的。例如,文档中的每一页都由一个互斥体来保护,只允许一个线程访问该页。无论用何种访问控制的方案,实现必须确保不会发生死锁(deadlock)情况。

应用程序(或系统)在多线程运行的环境中正常运行称为多线程安全(multi-thread safe)。确保多线程安全并不容易,实现必须使用之前提及的某种同步方案来实现互斥。

我们在这里讨论的并不是新内容,只有在涉及多进程时,同步才是个问题。不管怎样,一个进程不能访问另一个进程地址空间内的内容,这使得同步稍微容易一些。但是,进程中的线程共享进程所拥有的资源。因此,确保适当的同步非常重要。例如,在文档处理的应用程序中,如果打印线程已锁定页面,索引线程在访问相同页面之前必须等待,直到打印线程解锁页面。

在使用引用计数(reference counting)(也称为使用计数(use count))方案的情况下,多线程安全非常关键。引用计数方案将在后续章节中讨论。修改引用计数必须是一个线程安全的操作。

在多线程环境中使用对象时,多线程安全更加重要。如果不能确保多线程安全,可能会导致灾难。一个进程内的两个线程可以使用相同的对象。记住,所有对象都共享成员函数代码。当一个线程调用一个成员函数,在成员函数内部完成执行之前,如果(操作系统)调度(schedule)另一个线程运行,且该线程也通过相同的对象调用相同的成员函数,则对象必须保证自身完整和运行良好。如果对象不能做到这一点,这样的类就不是线程安全(thread-safe)的。当然,如果一个类(成员函数和数据成员)没有任何线程安全的特殊要求,维持线程安全就完全不成问题。

在设计新的类时,注意多线程安全非常重要。如果类的对象即使在多线程环境下都能保持完整,必须在类的文档中予以说明。另一方面,如果类的对象不保证多线程安全,也要在类的头文件和文档中清楚地说明其局限性。不要误认为设计的每个类都必须保证多线程安全,事实并非如此。是否需要线程安全取决于类和客户的要求。还需记住,X类如果使用其他类作为它实现的一部分,为保证X类为线程安全,有必要保证它使用的其他类都为线程安全。或者,即使它所依赖的其他类非线程安全,至少必须保证X类线程安全(这更加困难)。为达到线程安全,下面列举了一些指导原则:

(1)如果类声明为线程安全,确保每个成员函数实现也是线程安全的。

(2)如果类在实现中使用其他的类(对象),确保仍然能保证线程安全。

(3)如果使用一些类库来实现类,确保正在使用的库函数是线程安全的。

(4)如果正在使用操作系统调用,检查以确保这些调用都是线程安全的。

(5)当使用编译器提供的库时,检查它们是否都是线程安全的。

许多库的供应商提供辅助类,用于帮助达到线程安全。例如,查看提供线程安全引用计数的类十分常见。如果你的项目需要线程安全,它可能会帮助实现一组确保线程安全的低级类。这样的类可以提供引用计数、线程安全指针、线程安全打印实用程序等。如果整个项目小组都在各自的实现中使用这些类,就能保证整个项目的线程安全。

时间: 2024-08-18 05:26:31

《C++面向对象高效编程(第2版)》——2.20 什么是多线程安全类的相关文章

《C++面向对象高效编程(第2版)》——4.2 无用单元收集问题

4.2 无用单元收集问题 C++面向对象高效编程(第2版) 在我们讨论无用单元收集1(garbage collection)之前,先了解一下何为无用单元(garbage),何为悬挂引用(dangling reference). 4.2.1 无用单元 所谓无用单元(garbage),是一块存储区(或资源),该存储区虽然是程序(或进程)的一部分,但是在程序中却不可再对其引用.按照C++的规定,我们可以说,无用单元是程序中没有指针指向的某些资源.以下是一个示例: main() { char* p =

《C++面向对象高效编程(第2版)》——4.6 对象赋值的语义

4.6 对象赋值的语义 C++面向对象高效编程(第2版) 赋值与复制的操作非常类似.在C++中,绝大多数的复制操作都由语言隐式调用(当对象按值传递或按值返回时).当通过现有对象创建新对象时,也进行了复制操作(但不是很频繁).与复制相反的是,赋值是必须由程序员显式调用的操作.然而,在Eiffel和Smalltalk中,赋值和复制操作都由程序员显式调用.这也是基于值的语言与基于引用的语言之间的区别. 在C++中,对于对象和基本类型赋值都具有相同的含义.把基本类型变量赋值给另一个(兼容的)基本类型变量

《C++面向对象高效编程(第2版)》——2.21 确保抽象的可靠性——类不变式和断言

2.21 确保抽象的可靠性--类不变式和断言 C++面向对象高效编程(第2版) 任何抽象都必须与客户履行它的契约(contract).当客户使用类时,他希望类的对象像其发布描述的那样运行正常.另一方面,类的实现者必须千方百计地确保对象运行正常.但是,类只有在客户履行自己那部分契约后,才能正确行使它的职责.例如,类的成员函数可能要求传入的参数为非零指针(non-zero pointer).只有满足此前提条件,成员函数才能保证它的行为.因此,客户必须履行一些义务.换言之,如果客户履行了她那部分契约,

《C++面向对象高效编程(第2版)》——2.30 has-a关系的重要性

2.30 has-a关系的重要性 C++面向对象高效编程(第2版) "has-a"关系(也称为关联.聚集.包含.组合)是在OOD(面向对象设计)中频繁使用的重要关系.但是,许多设计者和程序员都没有很好地理解其相关性,从而导致复杂僵化的设计和不必要的继承. 在OOD阶段,软件的复用主要通过两种方式完成:继承和包含.它们各有优缺点.优秀的设计者了解它们的局限性.优点和代价,可以灵活自如地应用它们.继承将在第5章.第6章以及第二部分的第12章中详细讨论. 包含是一项强大的设计技术,它比继承更

《C++面向对象高效编程(第2版)》——2.27 关联

2.27 关联 C++面向对象高效编程(第2版) 关联表示对象与不同类之间的结构关系(structual relationship),大多数关联都是二元关系(binary relation).类之间的多重关联(multiple association)和类本身的自关联(self association)都是合法的(见图2-19). 关联可以有一个名称,表明阅读方向的箭头为可选.注意,方向箭头为可选,但关联名必须显示.关联在不同的方向可以有不同的名称,但是,大多数情况下,没必要注明(特别是在已标出

《C++面向对象高效编程(第2版)》——3.2 类要素的细节

3.2 类要素的细节 C++面向对象高效编程(第2版) 3.2.1 访问区域 客户可以访问在类的public区域中声明的任何成员.我们可以把该区域看做是通用公共(general public)的接口,它没有任何保护,是类限制最少的区域.一个设计良好的类绝不会将数据成员包含在public区域,该区域只能包含成员函数.如果在public区域包含数据成员,那么无需类的实现者,仅通过编译器即可访问这些数据成员.这违反了数据抽象和封装原则.这也是我们为什么总将数据成员放在private或protected

《C++面向对象高效编程(第2版)》——第2章 什么是数据抽象

第2章 什么是数据抽象 C++面向对象高效编程(第2版) 面向对象编程的一项基本任务是创建带有适当功能的类,并隐藏不必要的细节(即抽象数据).下面,我们将用一个现实生活中的例子来解释数据抽象的概念. 绝大多数人都见过影碟播放机(laser disc player)(或LD播放机).现在,提出一个简单的问题:设计一个影碟播放机,要求易于使用和修改,可后续添加更多有用的功能. 注意: 如果难以理解影碟播放机,可以用CD播放机代替LD播放机,其设计原理类似.实际上,影碟播放机的功能是CD播放机功能的超

《C++面向对象高效编程(第2版)》——1.5 什么可以作为类

1.5 什么可以作为类 C++面向对象高效编程(第2版) 用简单的例子详细讨论类和对象非常容易,但是难点在于如何为给定的问题找出合适的类.我们必须理解类代表什么,何时将问题中的某些部分转化为类,而非数据,反之亦然.根据我们的定义,类拥有一组对象的共同属性(或者特性).怎样的共同才是共同?何时说这是一个类,而不是另一个类的对象?这些都是我们在学习OOP时会遇到的,和真正关心的问题. 当我们决定创建一个类时,第一个问题就是"是否确实需要这个类的多个实例?",如果答案为"是&quo

《C++面向对象高效编程(第2版)》——4.3 C++中的无用单元收集

4.3 C++中的无用单元收集 C++面向对象高效编程(第2版) C++提供类的析构函数专门处理无用单元收集,但是,这并不意味着无用单元收集只发生在析构函数中.实际上,某些其他成员函数也必须考虑无用单元收集. 类的析构函数给予对象最后一次机会释放它所获得的所有资源.在退出某作用域之前,由语言自动为在该作用域中创建的自动(基于栈)对象调用析构函数.此时,对象即将被销毁(也就是说,被对象占用的内存即将被系统回收).一旦析构函数完成,对象将彻底地消失. 删除(使用delete操作符)指向某对象的指针时

《C++面向对象高效编程(第2版)》——2.10 抽象数据类型—栈的实现

2.10 抽象数据类型-栈的实现 C++面向对象高效编程(第2版) 下面的示例用于说明,在C中一个简单栈的实现. Stack.h文件--让所有的抽象数据类型用户都可以使用Stack. typedef Stack* Stackld; typedef int bool; struct Stack { int data; / 在栈上存储元素 */ unsigned count; / 栈上元素的数量 / int top; / 栈顶部的指针 */ / 略去其他细节 .../ };``` Stack.c文件