1.3 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来实现多线程。不过,由于缺乏统一标准的支持,以及统一的线程内存模型,从而会导致一些问题,这些问题在跨硬件或跨平台相关的多线程应用上表现得尤为明显。

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

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

    C++14中为并发和并行添加了一个新的互斥量类型,用于保护共享数据(参见第3章)。不过,在C++17就考虑的更多了:一开始就添加了一整套的并行算法(参见第10章)。两个标准将整个语言的标准库进行了补强,这也就让我们书写多线程代码更加的容易。

    之前我们还提到了一个并发技术规范,其描述C++标准对于函数和类的扩展,尤其是对线程同步方面(参见第4章)。

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

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

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

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

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

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

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