今天小编要说的是《C++并发编程实战》(第2版)这本书,很多程序员都知道这本书。第2版全新翻译,给你一个不一样的阅读体验。
《C++并发编程实战》(第2版)由C++标准委员会成员编写,囊括C++并发编程多个方面。作者Anthony Williams独自起草并参与编写了许多与多线程和并发相关的提案,这些提案塑造了C++标准的一部分。Anthony Williams持续参与了C++标准委员会并发小组的工作,包括对C++17标准进行改进,制定并发技术规约(Concurrency Technical Specification),以及编写关于C++未来演化发展的提案等。
- 这是一本介绍C++并发和多线编程的深度指南,囊括了C++并发编程的多个方面,涉及启动新线程以及设计全功能的多线程算法和数据结构等核心知识点;
- 本书译文经过反复推敲,作译者协同参与全书内容的翻译和审读,代码配有详细的中文注释,内容简洁易懂;
- 译者还基于自己的开发经验,补充了许多延伸知识点,适合想要深入了解C++多线程的开发人员深入学习;
- 本书提供强大的配套资源,包括近200页的电子版附录D以及140多份配套代码文件。
本书内容是 C++新标准中涉及的并发与多线程功能,从std::thread、std::mutex和std::async的基本使用方法开始,一直到复杂的内存模型和原子操作。
假若读者之前没使用过C++11的新功能,那就需要先浏览一下附录A,再开始阅读正文,这将有助于透彻理解本书的代码示例。正文中已经标注出用到C++新特性的地方,尽管如此,一旦你遇到任何从未见过的内容,也可以随时翻查附录。
如果读者已经编写过多线程代码,并且经验丰富,前几章会让你知晓已经熟知的工具与新标准的C++工具是怎样对应的。倘若读者要进行任何底层工作,涉及原子变量,则第5章必不可少。为了确保读者真正熟知C++多线程编程的各种细节,例如异常安全(exception safety),那么,第8章值得好好学习。如果读者肩负某种特定的编码任务,索引和目录会帮你迅速定位到有关章节。
即便你已经掌握了C++线程库的使用方法,附录D(可从异步社区下载)依然有用,例如可供你查阅各个类和函数调用的精准细节。你也可以考虑时不时地回顾一下主要章节,或强化记忆某个特定的模型,或重温示例代码。
简要目录
- 第1章 你好,C++并发世界
- 第2章 线程管控
- 第3章 在线程间共享数据
- 第4章 并发操作的同步
- 第5章 C++内存模型和原子操作
- 第6章 设计基于锁的并发数据结构
- 第7章 设计无锁数据结构
- 第8章 设计并发代码
- 第9章 高级线程管理
- 第10章 并行算法函数
- 第11章 多线程应用的测试和除错
样章试读:第1章 你好,C++并发世界
1.1 什么是并发
按最简单、最基本的程度理解,并发(concurrency)是两个或多个同时独立进行的活动。并发现象遍布日常生活,我们时常接触:我们可以边走路边说话;或者,左右手同时做出不一样的动作;我们每个人也都可以独立行事——当我游泳时,你可以观看足球比赛;诸如此类。
1.1.1 计算机系统中的并发
若我们谈及计算机系统中的并发,则是指同一个系统中,多个独立活动同时进行,而非依次进行。这不足为奇。多年来,多任务操作系统可以凭借任务切换,让同一台计算机同时运行多个应用软件,这早已稀松平常,而高端服务器配备了多处理器,实现了“真并发”(genuine concurrency)。大势所趋,主流计算机现已能够真真正正地并行处理多任务,而不再只是制造并发的表象。
很久之前,大多计算机都仅有一个处理器,处理器内只有单一处理单元或单个内核,许多台式计算机至今依旧如此。这种计算机在同一时刻实质上只能处理一个任务,不过,每秒内,它可以在各个任务之间多次切换,先处理某任务的一小部分,接着切换任务,同样只处理一小部分,然后对其他任务如法炮制。于是,看起来所有任务都正在同时执行。因此其被称为任务切换。至此,我们谈及的并发都基于这种模式。由于任务飞速切换,我们难以分辨处理器到底在哪一刻暂停某个任务而切换到另一个。任务切换对使用者和应用软件自身都制造出并发的表象。由于是表象,因此对比真正的并发环境,当应用程序在进行任务切换的单一处理器环境下运行时,其行为可能稍微不同。具体而言,如果就内存模型(见第5章)做出不当假设,本来会导致某些问题,但这些问题在上述环境中却有可能不会出现。第10章将对此深入讨论。
多年来,配备了多处理器的计算机一直被用作服务器,它要承担高性能的计算任务;现今,基于一芯多核处理器(简称多核处理器)的计算机日渐普及,多核处理器也用在台式计算机上。无论是装配多个处理器,还是单个多核处理器,或是多个多核处理器,这些计算机都能真正并行运作多个任务,我们称之为硬件并发(hardware concurrency)。
图1.1所示为理想化的情景。计算机有两个任务要处理,将它们进行十等分。在双核机(具有两个处理核)上,两个任务在各自的核上分别执行。另一台单核机则切换任务,交替执行任务小段,但任务小段之间略有间隔。在图1.1中,单核机的任务小段被灰色小条隔开,它们比双核机的分隔条粗大。为了交替执行,每当系统从某一个任务切换到另一个时,就必须完成一次上下文切换(context switch),于是耗费了时间。若要完成一次上下文切换,则操作系统需保存当前任务的CPU状态和指令指针[2],判定需要切换到哪个任务,并为之重新加载CPU状态。接着,CPU有可能需要将新任务的指令和数据从内存加载到缓存,这或许会妨碍CPU,令其无法执行任何指令,加剧延迟。
图1.1 两种并发方式:双核机上的并发执行与单核机上的任务切换
尽管多处理器或多核系统明显更适合硬件并发,不过有些处理器也能在单核上执行多线程。真正需要注意的关键因素是硬件支持的线程数(hardware threads),也就是硬件自身真正支持同时运行的独立任务的数量。即便是真正支持硬件并发的系统,任务的数量往往容易超过硬件本身可以并行处理的数量,因而在这种情形下任务切换依然有用。譬如,常见的台式计算机能够同时运行数百个任务,在后台进行各种操作,表面上却处于空闲状态。正是由于任务切换,后台任务才得以运作,才容许我们运行许多应用软件,如文字处理软件、编译器、编辑软件,以及浏览器等。图1.2展示了双核机上4个任务的相互切换,这同样是理想化的情形,各个任务都被均匀切分。实践中,许多问题会导致任务切分不均匀或调度不规则。我们将在第8章探究影响并发代码性能的因素,将解决上述某些问题。
图1.2 4个任务在双核机上切换
本书涉及的技术、函数和类适用于各种环境:无论负责运行的计算机是配备了单核单处理器,还是多核多处理器;无论其并发功能如何实现,是凭借任务切换,还是真正的硬件并发,一概不影响使用。然而,也许读者会想到,应用软件如何充分利用并发功能,很大程度上取决于硬件所支持的并发任务数量。我们将在第8章讲述设计并发的C++代码的相关议题,也会涉及这点。
1.1.2 并发的方式
设想两位开发者要共同开发一个软件项目。假设他们处于两间独立的办公室,而且各有一份参考手册,则他们可以静心工作,不会彼此干扰。但这令交流颇费周章:他们无法一转身就与对方交谈,遂不得不借助电话或邮件,或是需起身离座走到对方办公室。另外,使用两间办公室有额外开支,还需购买多份参考手册。
现在,如果安排两位开发者共处一室,他们就能畅谈软件项目的设计,也便于在纸上或壁板上作图,从而有助于交流设计的创意和理念。这样,仅有一间办公室要管理,并且各种资源通常只需一份就足够了。但缺点是,他们恐怕难以集中精神,共享资源也可能出现问题。
这两种安排开发者的办法示意了并发的两种基本方式。一位开发者代表一个线程,一间办公室代表一个进程。第一种方式采用多个进程,各进程都只含单一线程,情况类似于每位开发者都有自己的办公室;第二种方式只运行单一进程,内含多个线程,正如两位开发者同处一间办公室。我们可以随意组合这两种方式,掌控多个进程,其中有些进程包含多线程,有些进程只包含单一线程,但基本原理相同。接着,我们来简略看看应用软件中的这两种并发方式。
1.多进程并发
在应用软件内部,一种并发方式是,将一个应用软件拆分成多个独立进程同时运行,它们都只含单一线程,非常类似于同时运行浏览器和文字处理软件。这些独立进程可以通过所有常规的进程间通信途径相互传递信息(信号、套接字、文件、管道等),如图1.3所示。这种进程间通信普遍存在短处:或设置复杂,或速度慢,甚至二者兼有,因为操作系统往往要在进程之间提供大量防护措施,以免某进程意外改动另一个进程的数据;还有一个短处是运行多个进程的固定开销大,进程的启动花费时间,操作系统必须调配内部资源来管控进程,等等。
图1.3 两个进程并发运行并相互通信
进程间通信并非一无是处:通常,操作系统在进程间提供额外保护和高级通信机制。这就意味着,比起线程,采用进程更容易编写出安全的并发代码。某些编程环境以进程作为基本构建单元,其并发效果确实一流,譬如为Erlang编程语言准备的环境。
运用独立的进程实现并发,还有一个额外优势——通过网络连接,独立的进程能够在不同的计算机上运行。这样做虽然增加了通信开销,可是只要系统设计精良,此法足以低廉而有效地增强并发力度,改进性能。
2.多线程并发
另一种并发方式是在单一进程内运行多线程。线程非常像轻量级进程:每个线程都独立运行,并能各自执行不同的指令序列。不过,同一进程内的所有线程都共用相同的地址空间,且所有线程都能直接访问大部分数据。全局变量依然全局可见,指向对象或数据的指针和引用能在线程间传递。尽管进程间共享内存通常可行,但这种做法设置复杂,往往难以驾驭,原因是同一数据的地址在不同进程中不一定相同。图1.4展示了单一进程内的两个线程借共享内存通信。
图1.4 单一进程内的两个线程借共享内存通信
我们可以启用多个单线程的进程并在进程间通信,也可以在单一进程内发动多个线程而在线程间通信,后者的额外开销更低。因此,即使共享内存带来隐患,主流语言大都青睐以多线程的方式实现并发功能,当中也包括C++。再加上C++本身尚不直接支持进程间通信,所以采用多进程的应用软件将不得不依赖于平台专属的应用程序接口(Application Program Interface,API)。鉴于此,本书专攻多线程并发,后文再提及并发,便假定采用多线程实现。
提到多线程代码,还常常用到一个词——并行。接下来,我们来厘清并发与并行的区别。
1.1.3 并发与并行
就多线程代码而言,并发与并行(parallel)的含义很大程度上相互重叠。确实,在多数人看来,它们就是相同的。二者差别甚小,主要是着眼点和使用意图不同。两个术语都是指使用可调配的硬件资源同时运行多个任务,但并行更强调性能。当人们谈及并行时,主要关心的是利用可调配的硬件资源提升大规模数据处理的性能;当谈及并发时,主要关心的是分离关注点或响应能力。这两个术语之间并非泾渭分明,它们之间仍有很大程度的重叠,知晓这点会对后文的讨论有所帮助,两者的范例将穿插本书。
至此,我们已明晰并发的含义,现在来看看应用软件为什么要使用并发技术。
1.2 为什么使用并发技术
应用软件使用并发技术的主要原因有两个:分离关注点与性能提升。据我所知,实际上这几乎是仅有的用到并发技术的原因。如果寻根究底,任何其他原因都能归结为二者之一,也可能兼有(除非硬要说“因为我就是想并发”)。
1.2.1 为分离关注点而并发
一直以来,编写软件时,分离关注点(separation of concerns)几乎总是不错的构思:归类相关代码,隔离无关代码,使程序更易于理解和测试,因此所含缺陷很可能更少。并发技术可以用于隔离不同领域的操作,即便这些不同领域的操作需同时进行;若不直接使用并发技术,我们将不得不编写框架做任务切换,或者不得不在某个操作步骤中,频繁调用无关领域的代码。
考虑一个带有用户界面的应用软件,需要由CPU密集处理,如台式计算机上的DVD播放软件。本质上,这个应用软件肩负两大职责:既要从碟片读取数据,解码声音影像,并将其及时传送给图形硬件和音效硬件,让DVD顺畅放映,又要接收用户的操作输入,譬如用户按“暂停”“返回选项单”“退出”等键。假若采取单一线程,则该应用软件在播放过程中,不得不定时检查用户输入,结果会混杂播放DVD的代码与用户界面的代码。改用多线程就可以分离上述两个关注点,一个线程只负责用户界面管理,另一个线程只负责播放DVD,用户界面的代码和播放DVD的代码遂可避免紧密纠缠。两个线程之间还会保留必要的交互,例如按“暂停”键,不过这些交互仅仅与需要立即处理的事件直接关联。
如果用户发送了操作请求,而播放DVD线程正忙,无法马上处理,那么在请求被传送到该线程的同时,代码通常能令用户界面线程立刻做出响应,即便只是显示光标或提示“请稍候”。这种方法使得应用软件看起来响应及时。类似地,某些必须在后台持续工作的任务,则常常交由独立线程负责运行,例如,让桌面搜索应用软件监控文件系统变动。此法基本能大幅简化各线程的内部逻辑,原因是线程间交互得以限定于代码中可明确辨识的切入点,而无须将不同任务的逻辑交错散置。
这样,线程的实际数量便与CPU既有的内核数量无关,因为用线程分离关注点的依据是设计理念,不以增加运算吞吐量为目的。
1.2.2 为性能而并发:任务并行和数据并行
多处理器系统已存在数十年,不过一直以来它们大都只见于巨型计算机、大型计算机和大型服务器系统。但是,芯片厂家日益倾向设计多核芯片,在单一芯片上集成2个、4个、16个或更多处理器,从而使其性能优于单核芯片。于是,多核台式计算机日渐流行,甚至多核嵌入式设备亦然。不断增强的算力并非得益于单个任务的加速运行,而是来自多任务并行运作。从前,处理器更新换代,程序自然而然随之加速,程序员可以“坐享其成,不劳而获”。但现在,正如Herb Sutter指出的“免费午餐没有了![3]”,软件若要利用增强的这部分算力,就必须设计成并发运行任务。所以程序员必须警觉,特别是那些踌躇不前、忽视并发技术的同业,有必要注意熟练掌握并发技术,储备技能。
增强性能的并发方式有两种。第一种,最直观地,将单一任务分解成多个部分,各自并行运作,从而节省总运行耗时。此方式即为任务并行。尽管听起来浅白、直接,但这却有可能涉及相当复杂的处理过程,因为任务各部分之间也许存在纷繁的依赖。任务分解可以针对处理过程,调度某线程运行同一算法的某部分,另一线程则运行其他部分;也可以针对数据,线程分别对数据的不同部分执行同样的操作,这被称为数据并行。
易于采用上述并行方式的算法常常被称为尴尬并行[4]算法。其含义是,将算法的代码并行化实在简单,甚至简单得会让我们尴尬,实际上这是好事。我还遇见过用其他术语描述这类算法,叫“天然并行”(naturally parallel)与“方便并发”(conveniently concurrent)。尴尬并行算法具备的优良特性是可按规模伸缩——只要硬件支持的线程数目增加,算法的并行程度就能相应提升。这种算法是成语“众擎易举”的完美体现。算法中除尴尬并行以外的部分,可以另外划分成一类,其并行任务的数目固定(所以不可按规模伸缩)。第8章和第10章将涵盖按线程分解任务的方法。
第二种增强性能的并发方式是利用并行资源解决规模更大的问题。例如,只要条件适合,便同时处理2个文件,或者10个,甚至20个,而不是每次1个。同时对多组数据执行一样的操作,实际上是采用了数据并行,其着眼点有别于任务并行。采用这种方式处理单一数据所需的时间依旧不变,而同等时间内能处理的数据相对更多。这种方式明显存在局限,虽然并非任何情形都会因此受益,但数据吞吐量却有所增加,进而带来突破。例如,若能并行处理视频影像中不同的区域,就会提升视频处理的解析度。
1.2.3 什么时候避免并发
知道何时避免并发,与知道何时采用并发同等重要。归根结底,不用并发技术的唯一原因是收益不及代价。多数情况下,采用了并发技术的代码更难理解,编写和维护多线程代码会更劳心费神,并且复杂度增加可能导致更多错误。编写正确运行的多线程代码需要额外的开发时间和相关维护成本,除非潜在的性能提升或分离关注点而提高的清晰度值得这些开销,否则别使用并发技术。
此外,性能增幅可能不如预期。线程的启动存在固有开销,因为系统须妥善分配相关的内核资源和栈空间,然后才可以往调度器添加新线程,这些都会耗费时间。假如子线程上运行的任务太快完成,处理任务本身的时间就会远短于线程启动的时间,结果,应用程序的整体性能很可能还不如完全由主线程直接执行任务的性能。
再者,线程是一种有限的资源。若一次运行太多线程,便会消耗操作系统资源,可能令系统整体变慢。而且,由于每个线程都需要独立的栈空间[5],如果线程太多,就可能耗尽所属进程的可用内存或地址空间。在采用扁平模式内存架构的32位进程中,可用的地址空间是4GB,这很成问题:假定每个线程栈的大小都是1MB(这个大小常见于许多系统),那么4096个线程即会把全部地址空间耗尽,使得代码、静态数据和堆数据无地立足。尽管64位系统(或指令集宽度更大的系统)对地址空间的直接限制相对宽松,但其资源依旧有限,运行太多线程仍将带来问题。虽然线程池可用于控制线程数量(见第9章),但也非万能妙法,它自身也有局限。
假设,在服务器端,客户端/服务器(Client/Server,C/S)模式的应用程序为每个连接发起一个独立的线程。如果只有少量连接,这尚能良好工作。不过,请求量巨大的服务器需要处理的连接数目庞大,若采用同样的方法,就会发起过多线程而很快耗尽系统资源。针对这一情形,如果要达到最优性能,便须谨慎使用线程池(见第9章)。
最后,运行的线程越多,操作系统所做的上下文切换就越频繁,每一次切换都会减少本该用于实质工作的时间。结果,当线程数目达到一定程度时,再增加新线程只会降低应用软件的整体性能,而不会提升性能。正因如此,若读者意在追求最优系统性能,则须以可用的硬件并发资源为依据(或反之考虑其匮乏程度),调整运行线程的数目。
为了提升性能而使用并发技术,与其他优化策略相仿:它极具提升应用程序性能的潜力,却也可能令代码复杂化,使之更难理解、更容易出错。所以,对于应用程序中涉及性能的关键部分,若其具备提升性能的潜力,收效可观,才值得为之实现并发功能。当然,如果首要目标是设计得清楚明晰或分离关注点,而提升性能居次,也值得采用多线程设计。
倘若读者已决意在应用软件中使用并发技术,不论是为了性能,还是为了分离关注点,或是因为“多线程的良辰吉日已到”,那么这对C++程序员意义何在?
1.3 并发与C++多线程
以标准化形式借多线程支持并发是C++的新特性。C++11标准发布后,我们才不再依靠平台专属的扩展,可以用原生C++直接编写多线程代码。标准C++线程库的成型历经种种取舍,若要掌握其设计逻辑,则知晓其历史渊源颇为重要。
1.3.1 C++多线程简史
1998年发布的C++标准并没有采纳线程,许多语言要素在设定其操作效果之时,考虑的依据是抽象的串行计算机(sequential abstract machine)[6]。不仅如此,内存模型亦未正式定义,若不依靠编译器相关的扩展填补C++98标准的不足,就无法写出多线程程序。
为了支持多线程,编译器厂商便自行对C++进行扩展;广泛流行的C语言的多线程API,如符合POSIX规范的C语言多线程接口[7]和微软Windows系统的多线程API,使得许多C++厂商借助各种平台专属的扩展来支持多线程。这种来自编译器的支持普遍受限,在特定平台上只能使用相应的C语言API,并且须确保C++运行库(譬如异常处理机制的代码)在多线程场景下可以正常工作。尽管甚少编译器厂商给出了正规的多线程内存模型,但编译器和处理器运作优良,使数量庞大的多线程程序得以用C++写就。
C++程序员并不满足于使用平台专属的C语言API处理多线程,他们更期待C++类库提供面向对象的多线程工具。应用程序框架(如微软基础类库[8])和通用的C++程序库(如Boost和自适配通信环境[9])已累积开发了多个系列的C++类,封装了平台专属的底层API,并提供高级的多线程工具以简化编程任务。尽管C++类库的具体细节千差万别,特别是在启动新线程这一方面,但这些C++类的总体特征有很多共同之处。例如,通过资源获取即初始化(Resource Acquisition Is Initialization,RAII)的惯用手法进行锁操作,它确保了一旦脱离相关作用域,被锁的互斥就自行解开。这项设计特别重要,为许多C++类库所共有,使程序员受益良多。
现有的C++编译器在许多情况下都能支持多线程,再结合平台专属的API以及平台无关的类库,如Boost和ACE,为编写多线程的C++代码奠定了坚实的基础。于是,无数多线程应用的组件由C++写成,代码量庞大,以百万行计。不过它们缺乏统一标准的支持,这意味着,由于欠缺多线程内存模型,因此在某些情形下程序会出现问题,下面两种情形尤甚:依赖某特定的处理器硬件架构来获得性能提升,或是编写跨平台代码,但编译器的行为却因平台而异。
1.3.2 新标准对并发的支持
随着C++11标准的发布,上述种种弊端被一扫而空。C++标准库不仅规定了内存模型,可以区分不同线程,还扩增了新类,分别用于线程管控(见第2章)、保护共享数据(见第3章)、同步线程间操作(见第4章)以及底层原子操作(见第5章)等。
前文提及的几个C++类库在过往被使用过程中积累了很多经验,C++11线程库对它们颇为倚重。具体而言,新的C++库以Boost线程库作为原始范本,其中很多类在Boost线程库中存在对应者,名字和结构均一致。另外,Boost线程库自身做了多方面改动,以遵循C++标准。因此,原来的Boost使用者应该会对标准C++线程库深感熟悉。
正如本章开篇所述,C++11标准进行了多项革新,支持并发特性只是其中之一,语言自身还有很多的改进,以便程序员挥洒自如。虽然这些改进普遍超出本书范围,但其中一部分直接影响了C++线程库本身及其使用方式。附录A将简要介绍这些C++新特性。
1.3.3 C++14和C++17进一步支持并发和并行
C++14进一步增添了对并发和并行的支持,具体而言,是引入了一种用于保护共享数据的新互斥(见第3章)。C++17则增添了一系列适合新手的并行算法函数(见第10章)。这两版标准都强化了C++的核心和标准程序库的其他部分,简化了多线程代码的编写。
如前文所述,C++标准委员会还发布了并发技术规约,详述了对C++标准提供的类和函数所做的扩展,特别是有关线程间的同步操作。
C++明确规定了原子操作的语义,并予以直接支持,使开发者得以摆脱平台专属的汇编语言,仅用纯C++即可编写出高效的代码。对于力求编写高效且可移植的代码的开发者,这简直如有神助:不但由编译器负责处理平台的底层细节,还能通过优化器把操作语义也考虑在内,两者联手改进性能,使程序的整体优化效果更上一层楼。
1.3.4 标准C++线程库的效率
通常,对于从事高性能计算工作的开发者,无论是从整体上考量C++,还是就封装了底层工具的C++类而言(具体来说,以新的标准C++线程库中的类为例),他们最在意的因素通常是运行效率。若要实现某项功能,代码可以借助高级工具,或者直接使用底层工具。两种方式的运行开销不同,该项差异叫作抽象损失[10]。如果读者追求极致性能,清楚这点便尤为重要。
在设计C++标准库和标准C++线程库时,C++标准委员会对此非常注意。其中一项设计目标是,假定某些代码采用了C++标准库所提供的工具,如果改换为直接使用底层API,应该不会带来性能增益,或者收效甚微。因此,在绝大多数主流平台上,C++标准库得以高效地实现(低抽象损失)。
总有开发者追求性能极限,恨不得下探最底层,亲手掌控半导体器件,以穷尽计算机的算力。C++标准委员会的另一个目标是,确保C++提供充足的底层工具来满足需求。为此,新标准带来了新的内存模型,以及全方位的原子操作库,其能直接单独操作每个位、每个字节,还能直接管控线程同步,并让线程之间可以看见数据变更。过去,开发者若想深入底层,就得选用平台专属的汇编语言;现在,在许多场合,这些原子型别和对应的操作都足以取而代之。只要代码采用了新标准的数据型别与操作,便更具可移植性,且更容易维护。
C++标准库还提供了高级工具,抽象程度更高,更易于编写多线程代码,出错机会更少。使用这些工具必须执行额外的代码[11],所以有时确实会增加性能开销,但这种性能开销不一定会引发更多抽象损失。与之相比,实现同样的功能,手动编写代码所产生的开销往往更高。此外,对于上述绝大部分额外的代码,编译器会妥善进行内联。
针对某种特定的使用需求,一些高级工具提供了所需功能,有时还给出了额外功能,超出了原本的需求。在大多情况下,这都不成问题:未被使用的功能完全不产生开销。只有在极少数情况下,这些未被使用的功能会影响其他代码的性能。若读者追求卓越性能,无奈高级工具的开销过大,那最好还是利用底层工具亲自编写所需功能。在绝大部分情况中,这将导致复杂度和出错的可能性同时大增,而性能提升却十分有限,得益远远不偿所失。有时候,即便性能剖析表明瓶颈在于C++标准库的工具,但根本原因还是应用程序设计失当,而非类库的实现欠佳。譬如,如果太多线程争抢同一个互斥对象,就会严重影响性能。与其试图压缩互斥操作以节省琐碎时间,不如重新构建应用程序,从根本上减少对互斥对象的争抢,收效很可能更明显。第8章会涵盖上述议题:如何设计并发应用,减少资源争抢。
C++标准库还是有可能无法达到性能要求,无法提供所需的功能,但这种情况非常少,一旦出现这种情况,就似乎有必要使用平台专属的工具了。
1.3.5 平台专属的工具
虽然标准C++线程库给出了相当全面的多线程和并发工具,但在任何特定的平台上,总有平台专属的工具超额提供标准库之外的功能。为了可以便捷利用这些工具,同时又能照常使用标准C++线程库,C++线程库的某些型别有可能提供成员函数native_handle(),允许它的底层直接运用平台专属的API。因其本质使然,任何采用native_handle()的操作都完全依赖于特定平台,这也超出了本书的范围(以及C++标准库自身的范围)。