作为对《C++ Concurrency in Action》的中文翻译,本书是基于C++11新标准的并发和多线程编程深度指南。 从std::thread、std::mutex、std::future和std::async等基础类的使用,到内存模型和原子操作、基于锁和无锁数据结构的构建,再扩展到并行算法、线程管理,最后还介绍了多线程代码的测试工作。 本书的附录部分还对C++11新语言特性中与多线程相关的项目进行了简要的介绍,并提供了C++11线程库的完整参考。 本书适合于需要深入了解C++多线程开发的读者,以及使用C++进行各类软件开发的开发人员、测试人员。 对于使用第三方线程库的读者,也可以从本书后面的章节中了解到相关的指引和技巧。 同时,本书还可以作为C++11线程库的参考工具书。
AI Reading Assistant
Whole-book reading guide from stratified index samples; jump to passages in the text
Tags
AI categories
Shown after the reading guide is generated (max 3)
Text Preview (First 20 pages)
Registered users can read the full content for free
Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.
Page
1
http://showmecode.cn/links/book
Page
2
1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 1.10 1.11 1.12 1.13 1.14 1.15 1.16 1.17 1.18 1.19 Table of Contents Introduction 前言 关于封面 关于本书 第1章 你好,C++的并发世界 第2章 线程管理 第3章 线程间共享数据 第4章 同步并发操作 第5章 C++内存模型和原子类型操作 第6章 基于锁的并发数据结构设计 第7章 无锁并发数据结构设计 第8章 并发代码设计 第9章 高级线程管理 第10章 多线程程序的测试和调试 附录A C++11语言特性简明参考(部分) 附录B 并发库简要对比 附录C 消息传递框架与完整的ATM示例 附录D C++线程类库参考 资源 2 http://showmecode.cn/links/book
Page
3
C++ Concurrency In Action Practical Multithreading 作者:Anthony Williams 本书概述 作为对《C++ Concurrency in Action》的中文翻译。 本书是基于C++11新标准的并发和多线程编程深度指南。 从std::thread、std::mutex、std::future和std::async等基础类的使用,到内存模型和原子操 作、基于锁和无锁数据结构的构建,再扩展到并行算法、线程管理,最后还介绍了多线程代 码的测试工作。 本书的附录部分还对C++11新语言特性中与多线程相关的项目进行了简要的介绍,并提供了 C++11线程库的完整参考。 本书适合于需要深入了解C++多线程开发的读者,以及使用C++进行各类软件开发的开发人 员、测试人员。 对于使用第三方线程库的读者,也可以从本书后面的章节中了解到相关的指引和技巧。 同时,本书还可以作为C++11线程库的参考工具书。 书与作者 Anthony Williams是BSI C++小组的成员,拥有10多年C++应用经验。 如今多核芯处理器使用的越来越普遍。C++11标准支持多线程,这就需要程序员掌握多线程编 程的原则、技术和新语言中的并发特性,确保自己处于时代前沿。 无论你的C++技术如何,本书都会指引你使用C++11写出健壮和优雅的多线程应用。本书将会 探讨线程的内存模型,新的多线程库,启动线程和同步工具。在这个过程中,我们会了解并 发程序中较为棘手的一些问题。 内容的大体结构: C++11编程 Introduction 3 http://showmecode.cn/links/book
Page
4
多核芯编程 简单例子用于学习,复杂例子用于实践 本书是为C++程序员所写,同僚中可能有人对并发还没什么了解,估计也有人已经使用其他语 言、API或平台写过多线程程序。不过,在看本书的时候,你们都在同一“起跑线”上。 访问本书论坛曼宁-C++ Concurrency in Action可获取免费电子书。 本书相关 github 翻译地址:https://github.com/xiaoweiChen/Cpp_Concurrency_In_Action gitbook 在线阅读:http://chenxiaowei.gitbooks.io/cpp_concurrency_in_action/ 书中源码:https://github.com/bsmr-c-cpp/Cpp-Concurrency-in-Action 学习C++11/14: http://www.bogotobogo.com/cplusplus/C11 Introduction 4 http://showmecode.cn/links/book
Page
5
前言 我与多线程的邂逅是在毕业后的第一份工作中。那时我们正在写一个填充数据库的程序。不 过,需要处理的数据量很大,每条记录都是独立的,并且需要在插入数据库之前,对数据量 进行合理分配。为了充分利用10核UltraSPARC CPU(Ultra Scalable Processor ARChitecture,终极可扩充处理器架构(大端)),我们使用了多线程,每个线程处理自己所要 记录的数据。我们使用C++和POSIX线程库完成编码,也犯了一些错误——当时,多线程对 于我们来说是一个新事物——不过,最后我们还是完成了。也是在做这个项目的时候,我开 始注意C++标准委员会和刚刚发布的C++标准。 我对多线程和并发有着浓厚的兴趣。虽然,别人觉得多线程和并发难用、复杂,还会让代码 出现各种各样的问题,不过,在我看来这是一种强有力的工具,能让你充分使用硬件资源, 让你的程序运行的更快。 从那以后,我开始使用多线程和并发在单核机器上对应用性能和响应时间进行改善。这里, 多线程可以帮助你隐藏一些耗时的操作,比如I/O操作。同时,我也开始学习在操作系统级别 上使用多线程,并且了解Intel CPU如何处理任务切换。 同时,对C++的兴趣让我与ACCU有了联系,之后是BSI(英国标准委员会)中的C++标准委员 会,还有Boost。也是因为兴趣的原因,我参与了Boost线程库的初期开发工作,虽然初期版 本已经被开发者们放弃,但是我抓住了这次机会。直到现在,我依然是Boost线程库的主要开 发者和维护者。 作为C++标准委员会的一员,对现有标准的缺陷的和不足进行改善,并为新标准提出建议(新 标准命名为C++0x是希望它能在2009年发布,不过最后因为2011年才发布,所以官方命名为 C++11)。我也参与很多BSI的工作,并且我也为自己的建议起草建议书。当委员会将多线程提 上C++标准的日程时,我高兴的差点飞起来,因为我起草及合著的多线程和并发相关的草案, 将会成为新标uij准的一部分。新标准将我(计算机相关)的两大兴趣爱好——C++和多线程—— 结合起来,想想还有点小激动。 本书旨在教会其他C++开发者如何安全、高效的使用C++11线程库。我对C++和多线程的热 爱,希望你也能感受的到。 前言 5 http://showmecode.cn/links/book
Page
6
封面图片介绍 本书的封面图片的标题是“日本女性的着装”(Habit of a Lady of Japan)。这张图源自Thomas Jefferys所著的《不同民族服饰的收藏》(Collection of the Dress of Different Nations)[1]第四 卷(大概在1757年到1772年间出版)。Thomas收集的服饰包罗万象,他的绘画优美而又细腻, 对欧洲戏剧服装设计产生了长达200多年的影响。服饰中包含着一个文明的过去和现在,伦敦 剧院的观众通过不同的服饰对不同时代中各个国家的习俗。 在过去的250多年里,着装风格开始发生了变化,各个国家和区域之间巨大的差异逐渐消失。 现在已经很难分辨出不同洲不同地区的人们的着装差异。或许,我们放弃了这种文化上的差 异,得到的却是更加丰富多彩的个人生活——或者说是一种更加多样有趣、更快节奏的科技 生活。 在各种计算机图书铺天盖地、让人难以分辨的时代,Manning出版社正是为了赞美计算机行业 中的创新性和开拓性,才选用了这个重现两个世纪之前丰富多样的地域风情的图片。 【1】 《iPhone与iPad开发实战》使用了书中的另一张图片,感兴趣的同学可以去图灵社区 进行试读(只免费提供第1章内容),本章翻译复制了这本书翻译的部分内容 关于封面 6 http://showmecode.cn/links/book
Page
7
关于这本书 本书是并发和多线程机制指导书籍(基于C++11标准)。从最基本的 std::thread std::mutex 和 std::async 的使用,到复杂的原子操作和内存模型。 路线图 前4章,介绍了标准库提供的各种库工具,展示了使用方法。 第5章,涵盖了底层内存模型和原子操作的实际情况,包括原子操作如何对执行顺序进行限制 (这章标志着介绍部分的结束)。 第6、7章,开始讨论进阶级主题,如何使用基本工具去构建复杂的数据结构——第6章是基于 锁的数据结构,第7章是无锁数据结构。 第8章,对设计多线程代码给了一些指导意见,覆盖了性能问题和并行算法。 第9章,线程管理——线程池,工作队列和中断操作。 第10章,测试和调试——bug类型,定位Bug的技巧,以及如何进行测试等等。 附录,包括新的语言特性的简要描述,主要是与多线程相关的特性,以及在第4章中提到的消 息传递库的实现细节和C++11线程库的完整的参考。 谁应该读这本书 如果你正在用C++写一个多线程程序,你应该阅读本书。如果你正在使用C++标准库中新的多 线程工具,你可以从本书中得到一些指导意见。如果你正在使用其他线程库,后面章节里的 建议和技术指导也很值得一看。 阅读本书的前提是,你已经有了一个较好的C++基础知识;虽然,关于多线程编程的知识或者 经验是不必须的,不过这些经验可能有用。 如何使用这本书 如果从来没有写过多线程代码,我建议你从头到尾阅读本书;不过,可以跳过第5章中的较为 细节的部分。第7章内容依赖于第5章中的内容,因此,如果跳过了第5章,应该保证在读第7 章时,已经读过第5章。 如果没有用过C++11的工具,为了跟上这本书的进度,可以先阅读一下附录。新工具的使用在 文本中已经标注出来,不过,当遇到一些没见过的工具时,可以随时回看附录。 关于本书 7 http://showmecode.cn/links/book
Page
8
即使有不同环境下写多线程代码的经验,开始的章节仍有必要浏览一下,这样就能清楚的知 道,你所熟知的工具在新的C++标准中对应了哪些工具。如果使用原子变量去做一些底层工 作,第5章必须阅读。第8章,有关C++多线程的异常和安全性内容很值得一看。如果你对某 些关键词比较感兴趣,索引和目录能够帮你快速找到相关的内容。 你可能喜欢回顾主要的章节,并使用一个自己的方式阅读示例代码。虽然你已经了解C++线程 库,但附录D还是很有用。例如,查找每个类和函数的细节。 代码公约和下载 为了区分普通文本,列表中的所有代码和文本中的像这样固定宽度的字体。代码注释伴随着 许多列表,重要概念高亮显示。在某些情况下,你可以通过页下给出的快捷链接进行查阅。 本书所有实例的源代码,可在出版商的网站上进行下载: www.manning.com/cplusplusconcurrencyinaction。 软件需求 使用书中的代码,可能需要一个较新的C++编译器(要支持C++11语言的特性(见附录A)),还需 要C++支持标准线程库。 写本书的时候,g++是唯一实现标准线程库的编译器(尽管Microsoft Visual Studio 2011 preview中也有实现)。g++4.3发布时添加了线程库,并且在随后的发布版本中进行扩展。 g++4.3也支持部分C++11语言特性,更多特性的支持在后续发布版本中也有添加。更多细节 请参考g++ C++11的状态页面[1]。 Microsoft Visual Studio 2010支持部分C++11特性,例如:右值引用和lambda函数,但是没有 实现线程库。 我的公司Software Solutions Ltd,销售C++11标准线程库的完整实现,其可以使用在 Microsoft Visual Studio 2005, Microsoft Visual Studio 2008, Microsoft Visual Studio 2010, 以及各种g++版本上[2]。这个线程库也可以用来测试本书中的例子。 Boost线程库[3]提供的API,以及可移植到多个平台。本书中的大多数例子将 std:: 替换 为 boost:: ,再 #include 引用适当的头文件,就能使用Boost线程库来运行。还有部分工具 还不支持(例如 std::async )或在Boost线程库中有着不同名字(例 如: boost::unique_future )。 作者在线 关于本书 8 http://showmecode.cn/links/book
Page
9
购买C++ Concurrency in Action就能访问曼宁(Manning Publications)的私人网络论坛,在那 里可以对本书做一些评论,问一些技术问题,获得作者或其他读者的帮助。为了能够访问论 坛和订阅,在浏览器地址中输入www.manning.com/CPlusPlusConcurrencyinAction后,页面 将告诉你如何注册之后访问论坛,你能获得什么样的帮助,还有论坛中的一些规则。 曼宁保证为本书的读者提供互相交流,以及和作者交流的场所。虽然曼宁自愿维护本书的论 坛,但不保证这样的场所不会收取任何的费用。所以,建议你可以尝试提一些有挑战性的问 题给作者,免得这样的地方白白浪费。 在本书印刷时,就可以通过Internet访问作者的在线论坛和之前讨论的文字记录。 【1】GNU Compiler Collection C++0x/C++11 status page, http://gcc.gnu.org/projects/cxx0x.html. 【2】The just::thread implementation of the C++ Standard Thread Library, http://www.stdthread.co.uk. 【3】The Boost C++ library collection, http://www.boost.org. 关于本书 9 http://showmecode.cn/links/book
Page
10
第1章 你好,C++的并发世界! 本章主要内容 何谓并发和多线程 应用程序为什么要使用并发和多线程 C++的并发史 一个简单的C++多线程程序 令C++用户振奋的时刻到了。距初始的C++标准(1998年)发布13年后,C++标准委员会给 语言本身,以及标准库,带来了一次重大的变革。 新C++标准(也被称为C++11或C++0x)在2011年发布,带来一系列的变革让C++编程更加简 单和高效。 其中一个最重要的新特性就是对多线程的支持。 C++标准第一次承认多线程在语言中的存在,并在标准库中为多线程提供组件。这意味着使用 C++编写与平台无关的多线程程序成为可能,也为可移植性提供了强有力的保证。与此同时, 程序员们为提高应用的性能,对并发的关注也是与日俱增,特别在多线程编程方面。 本书是介绍如何使用C++11多线程来编写并发程序,及相关的语言特性和库工具(library facilities)。本书以“解释并发和多线程的含义,为什么要使用并发”作为起始点,在对“什么情况 下不使用并发”进行阐述之后,将对C++支持的并发方式进行概述;最后,以一个简单的 C++并发实例结束这一章。资深的多线程开发人员可以跳过前面的小节。在后面的几个章节 中,会有更多的例子,以便大家对库工具(library facilities)进行更加深入的了解。本书最后, 将会给出所有多线程与并发相关的C++标准库工具的全面参考。 问题来了,何谓并发(concurrency)?何谓多线程(multithreading)? 1.1 何谓并发 最简单和最基本的并发,是指两个或更多独立的活动同时发生。 并发在生活中随处可见,我们可以一边走路一边说话,也可以两只手同时作不同的动作,还 有我们每个人都过着相互独立的生活——当我在游泳的时候,你可以看球赛,等等。 1.1.1 计算机系统中的并发 计算机领域的并发指的是在单个系统里同时执行多个独立的任务,而非顺序的进行一些活 动。 第1章 你好,C++的并发世界 10 http://showmecode.cn/links/book
Page
11
计算机领域里,并发不是一个新事物:很多年前,一台计算机就能通过多任务操作系统的切 换功能,同时运行多个应用程序;高端多处理器服务器在很早就已经实现了真正的并行计 算。那“老东西”上有哪些“新东西”能让它在计算机领域越来越流行呢?——真正任务并行,而 非一种错觉。 以前,大多数计算机只有一个处理器,具有单个处理单元(processing unit)或核心(core),如今 还有很多这样的台式机。这种机器只能在某一时刻执行一个任务,不过它可以每秒进行多次 任务切换。通过“这个任务做一会,再切换到别的任务,再做一会儿”的方式,让任务看起来是 并行执行的。这种方式称为“任务切换(task switching)”。如今,我们仍然将这样的系统称为并发 (concurrency):因为任务切换得太快,以至于无法感觉到任务在何时会被暂时挂起,而切换到 另一个任务。任务切换会给用户和应用程序造成一种“并发的假象”。因为这种假象,当应用在 任务切换的环境下和真正并发环境下执行相比,行为还是有着微妙的不同。特别是对内存模 型不正确的假设(详见第5章),在多线程环境中可能不会出现(详见第10章)。 多处理器计算机用于服务器和高性能计算已有多年。基于单芯多核处理器(多核处理器)的台式 机,也越来越大众化。无论拥有几个处理器,这些机器都能够真正的并行多个任务。我们称 其为“硬件并发(hardware concurrency)”。 图1.1显示了一个计算机处理恰好两个任务时的理想情景,每个任务被分为10个相等大小的 块。在一个双核机器(具有两个处理核心)上,每个任务可以在各自的处理核心上执行。在单核 机器上做任务切换时,每个任务的块交织进行。但它们中间有一小段分隔(图中所示灰色分隔 条的厚度大于双核机器的分隔条);为了实现交织进行,系统每次从一个任务切换到另一个时都 需要切换一次上下文(context switch),任务切换也有时间开销。进行上下文的切换时,操作 系统必须为当前运行的任务保存CPU的状态和指令指针,并计算出要切换到哪个任务,并为 即将切换到的任务重新加载处理器状态。然后,CPU可能要将新任务的指令和数据的内存载 入到缓存中,这会阻止CPU执行任何指令,从而造成的更多的延迟。 图 1.1 并发的两种方式:双核机器的真正并行 Vs. 单核机器的任务切换 有些处理器可以在一个核心上执行多个线程,但硬件并发在多处理器或多核系统上效果更加显 著。硬件线程(hardware threads)最重要的因素是数量,也就是硬件上可以并发运行多少独 立的任务。即便是具有真正硬件并发的系统,也很容易拥有比硬件“可并行最大任务数”还要多 的任务需要执行,所以任务切换在这些情况下仍然适用。例如,在一个典型的台式计算机上 可能会有成百上千个的任务在运行,即便是在计算机处于空闲时,还是会有后台任务在运 行。正是任务切换使得这些后台任务可以运行,并使得你可以同时运行文字处理器、编译 第1章 你好,C++的并发世界 11 http://showmecode.cn/links/book
Page
12
器、编辑器和web浏览器(或其他应用的组合)。图1.2显示了四个任务在双核处理器上的任务切 换,仍然是将任务整齐地划分为同等大小块的理想情况。实际上,许多因素会使得分割不均 和调度不规则。部分因素将在第8章中讨论,那时我们再来看一看影响并行代码性能的因素。 无论应用程序在单核处理器,还是多核处理器上运行;也不论是任务切换还是真正的硬件并 发,这里提到的技术、功能和类(本书所涉及的)都能使用得到。如何使用并发,将很大程度上 取决于可用的硬件并发。我们将在第8章中再次讨论这个问题,并具体研究C++代码并行设计 的问题。 图 1.2 四个任务在两个核心之间的切换 1.1.2 并发的途径 试想当两个程序员在两个独立的办公室一起做一个软件项目,他们可以安静地工作、不互相 干扰,并且他们人手一套参考手册。但是,他们沟通起来就有些困难,比起可以直接互相交 谈,他们必须使用电话、电子邮件或到对方的办公室进行直接交流。并且,管理两个办公室 需要有一定的经费支出,还需要购买多份参考手册。 假设,让开发人员同在一间办公室办公,他们可以自由的对某个应用程序设计进行讨论,也 可以在纸或白板上轻易的绘制图表,对设计观点进行辅助性阐释。现在,你只需要管理一个 办公室,只要有一套参考资料就够了。遗憾的是,开发人员可能难以集中注意力,并且还可 能存在资源共享的问题(比如,“参考手册哪去了?”) 以上两种方法,描绘了并发的两种基本途径。每个开发人员代表一个线程,每个办公室代表 一个处理器。第一种途径是有多个单线程的进程,这就类似让每个开发人员拥有自己的办公 室,而第二种途径是在单一进程里有多个线程,如同一个办公室里有两个开发人员。让我们 在一个应用程序中简单的分析一下这两种途径。 多进程并发 使用并发的第一种方法,是将应用程序分为多个独立的进程,它们在同一时刻运行,就像同 时进行网页浏览和文字处理一样。如图1.3所示,独立的进程可以通过进程间常规的通信渠道 传递讯息(信号、套接字、文件、管道等等)。不过,这种进程之间的通信通常不是设置复杂, 就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以避免一个进程去修改 另一个进程的数据。还一个缺点是,运行多个进程所需的固定开销:需要时间启动进程,操 作系统需要内部资源来管理进程,等等。 第1章 你好,C++的并发世界 12 http://showmecode.cn/links/book
Page
13
当然,以上的机制也不是一无是处:操作系统在进程间提供的附加保护操作和更高级别的通 信机制,意味着可以更容易编写安全(safe)的并发代码。实际上,在类似于Erlang编程环境 中,将进程作为并发的基本构造块。 使用独立的进程,实现并发还有一个额外的优势———可以使用远程连接(可能需要联网)的方 式,在不同的机器上运行独立的进程。虽然,这增加了通信成本,但在设计精良的系统上, 这可能是一个提高并行可用行和性能的低成本方式。 图 1.3 一对并发运行的进程之间的通信 多线程并发 并发的另一个途径,在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独 立运行,且线程可以在不同的指令序列中运行。但是,进程中的所有线程都共享地址空间, 并且所有线程访问到大部分数据———全局变量仍然是全局的,指针、对象的引用或数据可 以在线程之间传递。虽然,进程之间通常共享内存,但这种共享通常也是难以建立,且难以 管理。因为,同一数据的内存地址在不同的进程中是不相同。图1.4展示了一个进程中的两个 线程通过共享内存进行通信。 图 1.4 同一进程中的一对并发运行的线程之间的通信 地址空间共享,以及缺少线程间数据的保护,使得操作系统的记录工作量减小,所以使用多 线程相关的开销远远小于使用多个进程。不过,共享内存的灵活性是有代价的:如果数据要 被多个线程访问,那么程序员必须确保每个线程所访问到的数据是一致的(在本书第3、4、5 第1章 你好,C++的并发世界 13 http://showmecode.cn/links/book
Page
14
和8章中会涉及,线程间数据共享可能会遇到的问题,以及如何使用工具来避免这些问题)。问 题并非无解,只要在编写代码时适当地注意即可,这同样也意味着需要对线程通信做大量的 工作。 多个单线程/进程间的通信(包含启动)要比单一进程中的多线程间的通信(包括启动)的开销大, 若不考虑共享内存可能会带来的问题,多线程将会成为主流语言(包括C++)更青睐的并发途 径。此外,C++标准并未对进程间通信提供任何原生支持,所以使用多进程的方式实现,这会 依赖与平台相关的API。因此,本书只关注使用多线程的并发,并且在此之后所提到“并发”, 均假设为多线程来实现。 了解并发后,让来看看为什么要使用并发。 1.2 为什么使用并发? 主要原因有两个:关注点分离(SOC)和性能。事实上,它们应该是使用并发的唯一原因;如果 你观察的足够仔细,所有因素都可以归结到其中的一个原因(或者可能是两个都有。当然,除 了像“就因为我愿意”这样的原因之外)。 1.2.1 为了分离关注点 编写软件时,分离关注点是个好主意;通过将相关的代码与无关的代码分离,可以使程序更 容易理解和测试,从而减少出错的可能性。即使一些功能区域中的操作需要在同一时刻发生 的情况下,依旧可以使用并发分离不同的功能区域;若不显式地使用并发,就得编写一个任 务切换框架,或者在操作中主动地调用一段不相关的代码。 考虑一个有用户界面的处理密集型应用——DVD播放程序。这样的应用程序,应具备这两种 功能:一,不仅要从光盘中读出数据,还需要对图像和声音进行解码,之后把解码出的信号 输出至视频和音频硬件,从而实现DVD的无误播放;二,还需要接受来自用户的输入,当用 户单击“暂停”、“返回菜单”或“退出”按键的时候执行对应的操作。当应用是单个线程时,应用 需要在回放期间定期检查用户的输入,这就需要把“DVD播放”代码和“用户界面”代码放在一 起,以便调用。如果使用多线程方式来分隔这些关注点,“用户界面”代码和“DVD播放”代码就 不再需要放在一起:一个线程可以处理“用户界面”事件,另一个进行“DVD播放”。它们之间会 有交互,例如,用户点击“暂停”,线程就可以直接进行认为交互。 这会给响应性带来一些错觉,因为用户界面线程通常可以立即响应用户的请求,在当请求传 达给忙碌线程,这时的相应可以是简单地显示代表忙碌的光标或“请等待”字样的消息。类似 地,独立的线程通常用来执行那些必须在后台持续运行的任务,例如,桌面搜索程序中监视 文件系统变化的任务。因为它们之间的交互清晰可辨,所以这种方式会使每个线程的逻辑变 的更加简单。 在这种情况下,线程的数量不再依赖CPU中的可用内核的数量,因为对线程的划分是基于概 念上的设计,而不是一种增加吞吐量的尝试。 第1章 你好,C++的并发世界 14 http://showmecode.cn/links/book
Page
15
1.2.2 为了性能 多处理器系统已经存在了几十年,但直到最近,它们也只在超级计算机、大型机和大型服务 器系统中才能看到。然而,芯片制造商越来越倾向于多核芯片的设计,即在单个芯片上集成 2、4、16或更多的处理器,从而获取更好的性能。因此,多核台式计算机、多核嵌入式设 备,现在越来越普遍。它们计算能力的提高不是源自使单一任务运行的更快,而是并行运行 多个任务。在过去,程序员曾坐看他们的程序随着处理器的更新换代而变得更快,无需他们 这边做任何事。但是现在,就像Herb Sutter所说的,“没有免费的午餐了。”[1] 如果想要利用 日益增长的计算能力,那就必须设计多任务并发式软件。程序员必须留意这个,尤其是那些 迄今都忽略并发的人们,现在很有必要将其加入工具箱中了。 两种方式利用并发提高性能:第一,将一个单个任务分成几部分,且各自并行运行,从而降 低总运行时间。这就是任务并行(task parallelism)。虽然这听起来很直观,但它是一个相当 复杂的过程,因为在各个部分之间可能存在着依赖。区别可能是在过程方面——一个线程执 行算法的一部分,而另一个线程执行算法的另一个部分——或是在数据方面——每个线程在 不同的数据部分上执行相同的操作(第二种方式)。后一种方法被称为数据并行(data parallelism)。 易受这种并行影响的算法常被称为易并行(embarrassingly parallel)。尽管你会受到易并行化 代码影响,但这对于你来说是一件好事:我曾遇到过自然并行(naturally parallel)和便利并发 (conveniently concurrent)的算法。易并行算法具有良好的可扩展特性——当可用硬件线程的 数量增加时,算法的并行性也会随之增加。这种算法能很好的体现人多力量大。如果算法中 有不易并行的部分,你可以把算法划分成固定(不可扩展)数量的并行任务。第8章将会再来讨 论,在线程之间划分任务的技巧。 第二种方法是使用可并行的方式,来解决更大的问题;与其同时处理一个文件,不如酌情处 理2个、10个或20个。虽然,这是数据并行的一种应用(通过对多组数据同时执行相同的操 作),但着重点不同。处理一个数据块仍然需要同样的时间,但在相同的时间内处理了更多的 数据。当然,这种方法也有限制,并非在所有情况下都是有益的。不过,这种方法所带来的 吞吐量提升,可以让某些新功能成为可能,例如,可以并行处理图片的各部分,就能提高视 频的分辨率。 1.2.3 什么时候不使用并发 知道何时不使用并发与知道何时使用它一样重要。基本上,不使用并发的唯一原因就是,收 益比不上成本。使用并发的代码在很多情况下难以理解,因此编写和维护的多线程代码就会 产生直接的脑力成本,同时额外的复杂性也可能引起更多的错误。除非潜在的性能增益足够 大或关注点分离地足够清晰,能抵消所需的额外的开发时间以及与维护多线程代码相关的额 外成本(代码正确的前提下);否则,别用并发。 第1章 你好,C++的并发世界 15 http://showmecode.cn/links/book
Page
16
同样地,性能增益可能会小于预期;因为操作系统需要分配内核相关资源和堆栈空间,所以 在启动线程时存在固有的开销,然后才能把新线程加入调度器中,所有这一切都需要时间。 如果在线程上的任务完成得很快,那么任务实际执行的时间要比启动线程的时间小很多,这 就会导致应用程序的整体性能还不如直接使用“产生线程”的方式。 此外,线程是有限的资源。如果让太多的线程同时运行,则会消耗很多操作系统资源,从而 使得操作系统整体上运行得更加缓慢。不仅如此,因为每个线程都需要一个独立的堆栈空 间,所以运行太多的线程也会耗尽进程的可用内存或地址空间。对于一个可用地址空间为 4GB(32bit)的平坦架构的进程来说,这的确是个问题:如果每个线程都有一个1MB的堆栈(很 多系统都会这样分配),那么4096个线程将会用尽所有地址空间,不会给代码、静态数据或者 堆数据留有任何空间。即便64位(或者更大)的系统不存在这种直接的地址空间限制,但其他资 源有限:如果你运行了太多的线程,最终也是出会问题的。尽管线程池(参见第9章)可以用来 限制线程的数量,但这并不是灵丹妙药,它也有自己的问题。 当客户端/服务器(C/S)应用在服务器端为每一个链接启动一个独立的线程,对于少量的链接是 可以正常工作的,但当同样的技术用于需要处理大量链接的高需求服务器时,也会因为线程 太多而耗尽系统资源。在这种场景下,谨慎地使用线程池可以对性能产生优化(参见第9 章)。 最后,运行越多的线程,操作系统就需要做越多的上下文切换,每个上下文切换都需要耗费 本可以花在有价值工作上的时间。所以在某些时候,增加一个额外的线程实际上会降低,而 非提高应用程序的整体性能。为此,如果你试图得到系统的最佳性能,可以考虑使用硬件并 发(或不用),并调整运行线程的数量。 为性能而使用并发就像所有其他优化策略一样:它拥有大幅度提高应用性能的潜力,但它也可 能使代码复杂化,使其更难理解,并更容易出错。因此,只有应用中具有显著增益潜力的性 能关键部分,才值得并发化。当然,如果性能收益的潜力仅次于设计清晰或关注点分离,可 能也值得使用多线程设计。 假设你已经决定确实要在应用程使用并发,无论是为了性能、关注点分离,亦或是因为“多线 程星期一”(multithreading Monday,)(译者:可能是学习多线程的意思)。 问题又来了,对于C++程序员来说,多线程意味着什么? 1.3 C++中使的并发和多线程 通过多线程为C++并发提供标准化支持是件新鲜事。只有在C++11标准下,才能编写不依赖平 台扩展的多线程代码。了解C++线程库中的众多规则,知晓其历史是很重要的。 1.3.1 C++多线程历史 第1章 你好,C++的并发世界 16 http://showmecode.cn/links/book
Page
17
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会对这些特性做一 些介绍。 第1章 你好,C++的并发世界 17 http://showmecode.cn/links/book
Page
18
新的C++标准直接支持原子操作,允许程序员通过定义语义的方式编写高效代码,而无需了解 与平台相关的汇编指令。这对于试图编写高效、可移植代码的程序员们来说是一个好消息; 不仅编译器可以搞定具体平台,还可以编写优化器来解释操作的语义,从而让程序整体得到 更好的优化。 1.3.3 C++线程库的效率 通常情况下,这是高性能计算开发者对C++的担忧之一。为了效率,C++类整合了一些底层工 具。了解性能的极限后,了解相关实现使用高级工具和直接低级工具的开销差,就显得十分 重要了。这个开销差就是抽象惩罚(abstraction penalty)。 C++标准委员会在整体设计标准库时,特别是在设计标准线程库的时候,就已经注意到了这 点;目标之一就是在提供相同的工具的情况下,直接使用底层API就完全得不到任何好处。因 此,该类库在大部分主流平台上都能实现高效(带有非常低的抽象惩罚)。 C++标准委员会的另一个目标,为了达到终极性能,需要确保C++能提供足够多的的底层工 具,给那些要与硬件打交道的程序员。为了这个目的,伴随着新的内存模型,出现了一个综 合的原子操作库,可用于直接控制单个位、字节、内部线程间同步,以及所有变化的可见 性。这些原子类型和相应的操作现在可以在很多地方使用,而这些地方以前可能使用的是平 台相关的汇编代码。使用了新的标准类型和操作代码具有更好的可移植性,而且更容易维 护。 C++标准库也提供了更高级别的抽象和工具,使得编写多线程代码更加简单,并且不易出错。 有时运用这些工具确实会带来性能开销,因为有额外的代码必须执行。但是,这种性能成本 并不一定意味着更高的抽象惩罚;总体来看,这种性能开销并不比手工编写等效函数高,而 且编译器可能会很好地内联大部分额外代码。 某些情况下,高级工具会提供一些额外的功能。大部分情况下这都不是问题,因为你没有为 你不使用的那部分买单。在罕见的情况下,这些未使用的功能会影响其他代码的性能。如果 你很看重程序的性能,并且高级工具带来的开销过高,你最好是通过较低级别的工具来手工 实现你需要的功能。在绝大多数情况下,因额外增加的复杂性而出错的几率都远大于性能的 小幅提升带来的收益。即便是有证据确实表明瓶颈出现在C++标准库的工具中,也可能会归咎 于低劣的应用设计,而非低劣的类库实现。例如,如果过多的线程竞争一个互斥单元,将会 很明显的影响性能。与其在互斥操作上耗费时间,不如重新设计应用,减少互斥元上的竞争 来的划算。如何减少应用中的竞争,会在第8章中再次提及。 在非常罕见的情况下,当C++标准库没有提供所需的性能或行为时,就有必要使用平台相关的 工具。 1.3.4 平台相关的工具 第1章 你好,C++的并发世界 18 http://showmecode.cn/links/book
Page
19
虽然C++线程库为多线程和并发处理提供了较全面的工具,但在某些平台上提供额外的工具。 为了方便的访问那些工具的同时,又使用标准C++线程库,在C++线程库中提供一 个 native_handle() 成员函数,允许通过使用平台相关API直接操作底层实现。就其本质而 言,任何使用 native_handle() 执行的操作是完全依赖于平台的,这超出了本书(同时也是标 准C++库本身)的范围。 所以,使用平台相关的工具之前,明白标准库能够做什么很重要,那么通过一个例子来开始 吧。 1.4 开始入门 ok!现在你有一个能与C++11标准兼容的编译器。接下来呢?一个C++多线程程序是什么样子 呢?其实,它看上去和其他C++程序差不多,通常是变量、类以及函数的组合。唯一的区别在 于某些函数可以并发运行,所以需要确保共享数据在并发访问时是安全的,详见第3章。当 然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。 1.4.1 你好,并发世界 从一个经典的例子开始:一个打印“Hello World.”的程序。一个非常简单的在单线程中运行的 Hello World程序如下所示,当我们谈到多线程时,它可以作为一个基准。 #include <iostream> int main() { std::cout << "Hello World\n"; } 这个程序所做的就是将“Hello World”写进标准输出流。让我们将它与下面清单所示的简单 的“Hello, Concurrent World”程序做个比较,它启动了一个独立的线程来显示这个信息。 清单 1.1 一个简单的Hello, Concurrent World程序: #include <iostream> #include <thread> //① void hello() //② { std::cout << "Hello Concurrent World\n"; } int main() { std::thread t(hello); //③ t.join(); //④ } 第1章 你好,C++的并发世界 19 http://showmecode.cn/links/book
Page
20
第一个区别是增加了 #include <thread> ①,标准C++库中对多线程支持的声明在新的头文件 中:管理线程的函数和类在 <thread> 中声明,而保护共享数据的函数和类在其他头文件中声 明。 其次,写信息的代码被移动到了一个独立的函数中②。因为每个线程都必须具有一个初始函数 (initial function),新线程的执行在这里开始。对于应用程序来说,初始线程是main(),但是对 于其他线程,可以在 std::thread 对象的构造函数中指定——在本例中,被命名为t③ 的 std::thread 对象拥有新函数hello()作为其初始函数。 下一个区别:与直接写入标准输出或是从main()调用hello()不同,该程序启动了一个全新的线 程来实现,将线程数量一分为二——初始线程始于main(),而新线程始于hello()。 新的线程启动之后③,初始线程继续执行。如果它不等待新线程结束,它就将自顾自地继续运 行到main()的结束,从而结束程序——有可能发生在新线程运行之前。这就是为什么在④这里 调用 join() 的原因——详见第2章,这会导致调用线程(在main()中)等待与 std::thread 对象 相关联的线程,即这个例子中的t。 如果这看起来仅仅为了将一条信息写入标准输出而做了大量的工作,确实如此——正如上文 1.2.3节所描述的,一般来说并不值得为了如此简单的任务而使用多线程,尤其是在这期间初 始线程并没做什么。本书后面的内容中,将通过实例来展示在哪些情景下使用多线程可以获 得明确的收益。 1.5 小结 本章中,提及了并发与多线程的含义,以及在你的应用程序中为什么你会选择使用(或不使用) 它。还提及了多线程在C++中的发展历程,从1998标准中完全缺乏支持,经历了各种平台相 关的扩展,再到新的C++11标准中具有合适的多线程支持。芯片制造商选择了以多核心的形 式,使得更多任务可以同时执行的方式来增加处理能力,而不是增加单个核心的执行速度。 在这个趋势下,C++多线程来的正是时候,它使得程序员们可以利用新的CPU,带来的更加 强大的硬件并发。 使用1.4节中例子,展示C++标准库中的类和函数有多么的简单。C++中使用多线程并不复 杂,复杂的是如何设计代码以实现其预期的行为。 尝试了1.4节的示例后,是时候看看更多实质性的内容了。 第2章中,我们将了解一下用于管理线程的类和函数。 [1] “The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software,” Herb Sutter, Dr. Dobb’s Journal, 30(3), March 2005. http://www.gotw.ca/publications/concurrency-ddj.htm. 第1章 你好,C++的并发世界 20 http://showmecode.cn/links/book
Loading comments...
Reply to Comment
Edit Comment