离线下载
PDF版 ePub版

陈晓伟 · 更新于 2018-11-28 11:00:42

1.3 C++中的并发和多线程

通过多线程为C++并发提供标准化支持是件新鲜事。只有在C++11标准下,才能编写不依赖平台扩展的多线程代码。了解C++线程库中的众多规则前,先来了解一下其发展的历史。

1.3.1 C++多线程历史

C++98(1998)标准不承认线程的存在,并且各种语言要素的操作效果都以顺序抽象机的形式编写。不仅如此,内存模型也没有正式定义,所以在C++98标准下,没办法在缺少编译器相关扩展的情况下编写多线程应用程序。

当然,编译器供应商可以自由地向语言添加扩展,添加C语言中流行的多线程API———POSIX标准中的C标准和Microsoft Windows API中的那些———这就使得很多C++编译器供应商通过各种平台相关扩展来支持多线程。这种编译器支持一般受限于只能使用平台相关的C语言API,并且该C++运行库(例如,异常处理机制的代码)能在多线程情况下正常工作。因为编译器和处理器的实际表现很不错了,所以在少数编译器供应商提供正式的多线程感知内存模型之前,程序员们已经编写了大量的C++多线程程序了。

由于不满足于使用平台相关的C语言API来处理多线程,C++程序员们希望使用的类库能提供面向对象的多线程工具。像MFC这样的应用框架,如同Boost和ACE这样的已积累了多组类的通用C++类库,这些类封装了底层的平台相关API,并提供用来简化任务的高级多线程工具。各种类和库在细节方面差异很大,但在启动新线程的方面,总体构造却大同小异。一个为许多C++类和库共有的设计,同时也是为程序员提供很大便利的设计,也就是使用带锁的获取资源即初始化(RAII, Resource Acquisition Is Initialization)的习惯,来确保当退出相关作用域时互斥元解锁。

编写多线程代码需要坚实的编程基础,当前的很多C++编译器为多线程编程者提供了对应(平台相关)的API;当然,还有一些与平台无关的C++类库(例如:Boost和ACE)。正因为如此,程序员们可以通过这些API来实现多线程应用。不过,由于缺乏统一标准的支持,缺少统一的线程内存模型,进而导致一些问题,这些问题在跨硬件或跨平台相关的多线程应用上表现得尤为明显。

1.3.2 新标准支持并发

所有的这些随着C++11标准的发布而改变了,新标准中不仅有了一个全新的线程感知内存模型,C++标准库也扩展了:包含了用于管理线程(参见第2章)、保护共享数据(参见第3章)、线程间同步操作(参见第4章),以及低级原子操作(参见第5章)的各种类。

新C++线程库很大程度上,是基于上文提到的C++类库的经验积累。特别是,Boost线程库作为新类库的主要模型,很多类与Boost库中的相关类有着相同名称和结构。随着C++标准的进步,Boost线程库也配合着C++标准在许多方面做出改变,因此之前使用Boost的用户将会发现自己非常熟悉C++11的线程库。

如本章起始提到的那样,支持并发仅仅是C++标准的变化之一,此外还有很多对于编程语言自身的改善,就是为了让程序员们的工作变得更加轻松。这些内容在本书的论述范围之外,但是其中的一些变化对于线程库本身及其使用方式产生了很大的影响。附录A会对这些特性做一些介绍。

新的C++标准直接支持原子操作,允许程序员通过定义语义的方式编写高效的代码,从而无需了解与平台相关的汇编指令。这对于试图编写高效、可移植代码的程序员们来说是一个好消息;编译器不仅可以搞定具体平台,还可以编写优化器来解释操作语义,从而让程序整体得到更好的优化。

1.3.3 C++线程库的效率

通常情况下,这是高性能计算开发者对C++的担忧之一。为了效率,C++类整合了一些底层工具。这样就需要了解相关使用高级工具和使用低级工具的开销差,这个开销差就是抽象代价(abstraction penalty)。

C++标准委员会在设计标准库时,特别是设计标准线程库的时候,就已经注意到了这点;目的就是在实现相同功能的前提下,直接使用底层API并不会带来过多的性能收益。因此,该类库在大部分主流平台上都能实现高效(带有非常低的抽象代价)。

C++标准委员会为了达到终极性能,需要确保C++能给那些要与硬件打交道的程序员,提供足够多的的底层工具。为了这个目的,伴随着新的内存模型,出现了一个综合的原子操作库,可用于直接控制单个位、字节、内部线程间同步,以及所有变化的可见性。原子类型和相应的操作现在可以在很多地方使用,而这些地方以前可能使用的是平台相关的汇编代码。使用了新标准的代码会具有更好的可移植性,而且更容易维护。

C++标准库也提供了更高级别的抽象和工具,使得编写多线程代码更加简单,并且不易出错。有时运用这些工具确实会带来性能开销,因为有额外的代码必须执行。但是,这种性能成本并不一定意味着更高的抽象代价;总体来看,这种性能开销并不比手工编写等效函数高,而且编译器可能会很好地内联大部分额外代码。

某些情况下,高级工具会提供一些额外的功能。大部分情况下这都不是问题,因为你没有为你不使用的那部分买单。在罕见的情况下,这些未使用的功能会影响其他代码的性能。如果你很看重程序的性能,并且高级工具带来的开销过高,你最好是通过较低级别的工具来实现你需要的功能。绝大多数情况下,额外增加的复杂性和出错几率都远大于性能的小幅提升带来的收益。即便是有证据确实表明瓶颈出现在C++标准库的工具中,也可能会归咎于低劣的应用设计,而非低劣的类库实现。例如,如果过多的线程竞争一个互斥单元,将会很明显的影响性能。与其在互斥操作上耗费时间,不如重新设计应用,减少互斥元上的竞争来得划算。如何减少应用中的竞争,会在第8章中再次提及。

在C++标准库没有提供所需的性能或行为时,就需要使用与平台相关的工具。

1.3.4 平台相关的工具

虽然C++线程库为多线程和并发处理提供了较全面的工具,但在某些平台上提供额外的工具。为了方便地访问那些工具的同时,又使用标准C++线程库,在C++线程库中提供一个native_handle()成员函数,允许通过使用平台相关API直接操作底层实现。就其本质而言,任何使用native_handle()执行的操作都是完全依赖于平台的,这超出了本书(同时也是标准C++库本身)的范围。

所以,使用平台相关的工具之前,要明白标准库能够做什么,那么下面通过一个栗子来展示下吧。