使用C++11标准,可以编写不依赖平台扩展的多线程代码。了解C++线程库前,先来了解一下C++多线程的发展史。
C++98(1998)标准不承认线程的存在,并且各种语义以顺序抽象的形式编写。不仅如此,也没有内存模型,所以C++98标准在缺少编译器扩展的情况下,没办法编写多线程应用。
当然,编译器供应商可以自由地向语言添加扩展,C语言中流行的多线程API——POSIX标准中的C标准和Microsoft Windows API——很多C++编译器供应商,通过各种平台相关的扩展来支持多线程。这种支持受限于平台,并且需要相应平台的运行库(例如,异常处理机制的代码)能在多线程情况下正常工作。因为编译器和处理器的实际表现很不错,所以在少数编译器供应商提供正式的多线程内存模型之前,开发者们已经写了很多的C++多线程程序了。
由于不满足于使用平台相关的API来处理多线程,C++开发者们希望使用面向对象的多线程工具。像MFC这样的应用框架,和Boost和ACE这样的通用库,这些库提供了很多简化任务的多线程工具。各种库在细节方面差异很大,但在启动线程的方面,却大同小异。其使用一种便利的设计,也就是使用带锁的获取资源即初始化(RAII, Resource Acquisition Is Initialization)的方式。
编写多线程代码需要扎实的编程基础,当前的很多C++编译器为多线程编程者提供了对应的API,还有一些与平台无关的C++库。这样,开发者们就可以通过这些API来实现多线程。不过,由于缺乏统一的标准,以及内存模型,就会产生一些问题,这些问题在跨平台的多线程应用上表现得尤为明显。
这些随着C++11标准的发布而改变,新标准中不仅有了全新的内存模型,C++标准库也扩展了:管理线程(参见第2章)、保护共享数据(参见第3章)、线程间同步操作(参见第4章),以及原子操作(参见第5章)。
标准线程库很大程度上,基于之前C++库的经验积累。特别是Boost线程库,作为新标准库的很多类与Boost库中的相关类有着相同名称和结构。随着C++标准的进步,Boost线程库也随着C++标准在许多方面做出改变,这样之前使用Boost的用户会发现自己非常熟悉C++11的线程库。
支持并发仅是C++11标准的变化之一,为了让开发者们的工作变得更加轻松,还有很多对于语言自身的改善。这些内容不在本书的讨论范围内,但是其中的一些变化对线程库及其使用方式有着很大的影响。附录A会对这些特性做一些介绍。
C++14中为并发和并行添加了一个新的互斥量类型,用于保护共享数据(参见第3章)。C++17考虑的更多:添加了一整套的并行算法(参见第10章)。两个标准将整个标准库进行了补强,这让书写多线程代码变得更加容易。
之前还提到了一个并发技术标准,其描述C++标准对于函数和类的扩展,尤其是对线程同步方面(参见第4章)。
C++新标准直接支持原子操作,允许开发者通过指定语义的方式编写代码,从而无需了解与平台相关的汇编指令。这对于编写高效、可移植的代码来说,无疑是一个好消息。编译器不仅可以搞定具体平台,还可以编写优化器来解释操作语义,从而让程序得到更好的优化。
这是高性能计算开发者的担忧之一。为了效率,C++整合了一些底层工具。这样就需要了解使用高级工具和使用低级工具的开销差,这个开销差就是抽象代价(abstraction penalty)。
C++标准委员会在设计标准库时,特别是线程库,就注意到了这点。目的就是在实现相同功能的前提下,确保使用高级API和使用底层API带来的性能收益相当。这样,标准库在主流平台上都能有高效实现(带有非常低的抽象代价)。
为了达到终极性能,需要给与硬件打交道的开发者提供足够多的底层工具。为了这个目的,形成了原子操作库,可直接控制单个位、字节、内部线程间的同步,以及对所有变化的可见性。原子类型可以在很多地方使用,使用新标准的代码会有更好的可移植性,并且容易维护。
标准库也提供了更高级别工具,使得编写多线程代码更加简单。因为有额外的代码需要执行,这些工具确实会带来性能开销。总的来说,性能开销和手工编写的函数差不多,并且编译器会内联大部分代码。
某些情况下,高级工具会提供一些额外的功能。极少的情况下,一些未使用的功能会影响其他代码的性能。如果很看重程序的性能,并且高级工具带来的开销过高,最好是通过较低层工具来实现功能。绝大多数情况下,用过高的复杂性和过大的出错率,来交换小幅度的性能收益是不划算的。即便是瓶颈出现在C++标准库的工具中,也可能由低劣的程序设计造成。例如,如果过多的线程竞争一个互斥单元,将会很明显的影响性能。与其在互斥操作上耗费时间,不如重新设计,减少互斥单元上的竞争。
C++标准库没有提供所需的性能或行为时,就需要使用与平台相关的工具了。
虽然C++线程库为多线程和并发处理提供了较全面的工具,但某些平台会提供额外的工具。为了方便地使用这些工具,又使用标准C++线程库,在C++线程库中提供一个native_handle()
的成员函数,允许通过使用平台相关API直接操作底层实现。就其本质而言,任何使用native_handle()
执行的操作都是完全依赖于平台,这也超出了本书(同时也是标准C++库本身)讨论的范围。
所以,使用平台相关的工具之前,先了解一下标准库能够做些什么吧。