本节书摘来自异步社区出版社《C++并发编程实战》一书中的第1章,第1.1节,作者:【美】 Anthony Williams (威廉姆斯),更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.3 在C++中使用并发和多线程
通过多线程为并发提供标准化的支持对C++来说是新鲜事物。只有在即将到来的C++11标准中,你才能不依赖平台相关的扩展来编写多线程代码。为了理解新版本C++线程库中众多规则背后的基本原理,了解其历史是很重要的。
1.3.1 C++多线程历程
1998C++标准版不承认线程的存在,并且各种语言要素的操作效果都以顺序抽象机的形式编写。不仅如此,内存模型也没有被正式定义,所以对于1998 C++标准,你没办法在缺少编译器相关扩展的情况下编写多线程应用程序。
当然,编译器供应商可以自由地向语言添加扩展,并且针对多线程的C API的流行——例如在POSIX C和Microsoft Windows API中的那些——导致很多C++编译器供应商通过各种平台相关的扩展来支持多线程。这种编译器支持普遍地受限于只允许使用该平台相应的C API以及确保该C++运行时库(例如异常处理机制的代码)在多线程存在的情况下运行。尽管极少有编译器供应商提供了一个正式的多线程感知内存模型,但编译器和处理器的实际表现也已经足够好,以至于大量的多线程的C++程序已被编写出来。
由于不满足于使用平台相关的C API来处理多线程,C++程序员曾期望他们的类库提供面向对象的多线程工具。像MFC这样的应用程序框架,以及像Boost和ACE这样的C++通用类库曾积累了多套C++类,封装了下层的平台相关API并提供高级的多线程工具以简化任务。各类库的具体细节,特别是在启动新线程的方面,存在很大差异,但是这些类的总体构造存在很多共通之处。有一个为许多C++类库共有的,同时也是为程序员提供很大便利的特别重要的设计,就是带锁的资源获得即初始化(RAII, ResourceAcquisitionIsInitialization)的习惯用法,来确保当退出相关作用域的时候互斥元被解锁。
许多情况下,现有的C++编译器所提供的多线程支持,例如Boost和ACE,综合了平台相关API以及平台无关类库的可用性,为编写多线程C++代码提供一个坚实的基础,也因此大约有数百万行C++代码作为多线程应用程序的一部分而被编写出来。但缺乏标准的支持,意味着存在缺少线程感知内存模型从而导致问题的场合,特别是对于那些试图通过使用处理器硬件能力来获取更高性能,或是编写跨平台代码,但是在不同平台之间编译器的实际表现存在差异。
1.3.2 新标准中的并发支持
所有这些都随着新的C++11标准的发布而改变了。不仅有了一个全新的线程感知内存模型,C++标准库也被扩展了,包含了用于管理线程(参见第2章)、保护共享数据(参见第3章)、线程间同步操作(参见第4章)以及低级原子操作(参见第5章)的各个类。
新的C++线程库很大程度上基于之前通过使用上文提到的C++类库而积累的经验。特别地,Boost线程库被用作新类库所基于的主要模型,很多类与Boost中的对应者共享命名和结构。在新标准演进的过程中,这是个双向流动,Boost线程库也改变了自己,以便在多个方面匹配C++标准,因此从Boost迁移过来的用户将会发现自己非常适应。
正如本章开篇提到的那样,对并发的支持仅仅是新C++标准的变化之一,此外还存在很多对于编程语言自身的改善,可以使得程序员们的工作更便捷。这些内容虽然不在本书的论述范围之内,但是其中的一些变化对于线程库本身及其使用方式已经形成了直接的冲击。附录A对这些语言特性做了简要的介绍。
C++中对原子操作的直接支持,允许程序员编写具有确定语义的高效代码,而无需平台相关的汇编语言。这对于那些试图编写高效的、可移植代码的程序员们来说是一个真正的福利。不仅有编译器可以搞定平台的具体内容,还可以编写优化器来考虑操作的语义,从而让程序作为一个整体得到更好的优化。
1.3.3 C++线程库的效率
对于C++整体以及包含低级工具的C++类——特别是在新版C++线程库里的那些,参与高性能计算的开发者常常关注的一点就是效率。如果你正寻求极致的性能,那么理解与直接使用底层的低级工具相比,使用高级工具所带来的实现成本,是很重要的。这个成本就是抽象惩罚(abstractionpenalty)。
C++标准委员会在整体设计C++标准库以及专门设计标准C++线程库的时候,就已经十分注重这一点了。其设计的目标之一就是在提供相同的工具时,通过直接使用低级API就几乎或完全得不到任何好处。因此该类库被设计为在大部分平台上都能高效实现(带有非常低的抽象惩罚)。
C++标准委员会的另一个目标,是确保C++能提供足够的低级工具给那些希望与硬件工作得更紧密的程序员,以获取终极性能。为了达到这个目的,伴随着新的内存模型,出现了一个全面的原子操作库,用于直接控制单个位、字节、线程间同步以及所有变化的可见性。这些原子类型和相应的操作现在可以在很多地方加以使用,而这些地方以前通常被开发者选择下放到平台相关的汇编语言中。使用了新的标准类型和操作的代码因而具有更佳的可移植性,并且更易于维护。
C++标准库也提供了更高级别的抽象和工具,它们使得编写多线程代码更简单和不易出错。有时候运用这些工具确实会带来性能成本,因为必须执行额外的代码。但是这种性能成本并不一定意味着更高的抽象惩罚;总体来看,这种性能成本并不比通过手工编写等效的函数而招致的成本更高,同时编译器可能会很好地内联大部分额外的代码。
在某些情况下,高级工具提供超出特定使用需求的额外功能。在大部分情况下这都不是问题,你没有为你不使用的那部分买单。在罕见的情况下,这些未使用的功能会影响其他代码的性能。如果你更看重程序的性能,且代价过高,你可能最好是通过较低级别的工具来手工实现需要的功能。在绝大多数情况下,额外增加的复杂性和出错的几率远大于小小的性能提升所带来的潜在收益。即使有证据确实表明瓶颈出现在C++标准库的工具中,这也可能归咎于低劣的应用程序设计而非低劣的类库实现。例如,如果过多的线程竞争一个互斥元,这将会显著影响性能。与其试图在互斥操作上花掉一点点的时间,还不如重新构造应用程序以减少互斥元上的竞争来得划算。设计应用程序以减少竞争会在第8章中加以阐述。
在非常罕见的情况下,C++标准库不提供所需的性能或行为,这时则有必要运用特定的平台相关的工具。
1.3.4 平台相关的工具
虽然C++线程库为多线程和并发处理提供了颇为全面的工具,但是在所有的平台上,都会有些额外的平台相关工具。为了能方便地访问那些工具而又不用放弃使用标准C++线程库带来的好处,C++线程库中的类型可以提供一个·native_handle()
·成员函数,允许通过使用平台相关API直接操作底层实现。就其本质而言,任何使用·native_handle()
·执行的操作是完全依赖于平台的,这也超出了本书(同时也是标准C++库本身)的范围。
当然,在考虑使用平台相关的工具之前,明白标准库能够提供什么是很重要的,那么让我们通过一个例子来开始。