├── .gitignore ├── LICENSE ├── README.md ├── SUMMARY.md ├── _config.yml ├── config.json ├── content ├── about_cover_illustration │ └── about_cover_illustration-chinese.md ├── about_this_book │ └── about_this_book-chinese.md ├── appendix_A │ ├── A.0-chinese.md │ ├── A.1-chinese.md │ ├── A.2-chinese.md │ ├── A.3-chinese.md │ ├── A.4-chinese.md │ ├── A.5-chinese.md │ ├── A.6-chinese.md │ ├── A.7-chinese.md │ ├── A.8-chinese.md │ └── A.9-chinese.md ├── appendix_B │ └── B.0-chinese.md ├── appendix_C │ └── C.0-chinese.md ├── appendix_D │ ├── D.0-chinese.md │ ├── D.1-chinese.md │ ├── D.2-chinese.md │ ├── D.3-chinese.md │ ├── D.4-chinese.md │ ├── D.5-chinese.md │ ├── D.6-chinese.md │ └── D.7-chinese.md ├── chapter1 │ ├── 1.0-chinese.md │ ├── 1.1-chinese.md │ ├── 1.2-chinese.md │ ├── 1.3-chinese.md │ ├── 1.4-chinese.md │ └── 1.5-chinese.md ├── chapter10 │ ├── 10.0-chinese.md │ ├── 10.1-chinese.md │ ├── 10.2-chinese.md │ └── 10.3-chinese.md ├── chapter2 │ ├── 2.0-chinese.md │ ├── 2.1-chinese.md │ ├── 2.2-chinese.md │ ├── 2.3-chinese.md │ ├── 2.4-chinese.md │ ├── 2.5-chinese.md │ └── 2.6-chinese.md ├── chapter3 │ ├── 3.0-chinese.md │ ├── 3.1-chinese.md │ ├── 3.2-chinese.md │ ├── 3.3-chinese.md │ └── 3.4-chinese.md ├── chapter4 │ ├── 4.0-chinese.md │ ├── 4.1-chinese.md │ ├── 4.2-chinese.md │ ├── 4.3-chinese.md │ ├── 4.4-chinese.md │ └── 4.5-chinese.md ├── chapter5 │ ├── 5.0-chinese.md │ ├── 5.1-chinese.md │ ├── 5.2-chinese.md │ ├── 5.3-chinese.md │ └── 5.4-chinese.md ├── chapter6 │ ├── 6.0-chinese.md │ ├── 6.1-chinese.md │ ├── 6.2-chinese.md │ ├── 6.3-chinese.md │ └── 6.4-chinese.md ├── chapter7 │ ├── 7.0-chinese.md │ ├── 7.1-chinese.md │ ├── 7.2-chinese.md │ ├── 7.3-chinese.md │ └── 7.4-chinese.md ├── chapter8 │ ├── 8.0-chinese.md │ ├── 8.1-chinese.md │ ├── 8.2-chinese.md │ ├── 8.3-chinese.md │ ├── 8.4-chinese.md │ ├── 8.5-chinese.md │ └── 8.6-chinese.md ├── chapter9 │ ├── 9.0-chinese.md │ ├── 9.1-chinese.md │ ├── 9.2-chinese.md │ └── 9.3-chinese.md ├── preface │ └── preface-chinese.md └── resources │ └── resource.md ├── cover.jpg ├── cover ├── background.jpg └── logo.png └── images ├── chapter1 ├── 1-1.png ├── 1-2.png ├── 1-3.png └── 1-4.png ├── chapter3 └── 3-1.png ├── chapter4 ├── 4-1.png ├── 4-2.png └── 4-3.png ├── chapter5 ├── 5-1.png ├── 5-2.png ├── 5-3-table.png ├── 5-3.png ├── 5-4.png ├── 5-5.png ├── 5-6.png └── 5-7.png ├── chapter6 └── 6-1.png ├── chapter7 └── 7-1.png └── chapter8 ├── 8-1.png ├── 8-2.png ├── 8-3.png └── amdahl_law.png /.gitignore: -------------------------------------------------------------------------------- 1 | /_book/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | C++ Concurrency In Action 2 | ========================= 3 | *Practical Multithreading* 4 | ------------------------- 5 | - 作者:Anthony Williams 6 | - 译者:陈晓伟 7 | 8 | ## 本书概述 9 | 10 | 作为对《C++ Concurrency in Action》的中文翻译。 11 | 12 | 本书是基于C++11新标准的并发和多线程编程深度指南。 13 | 14 | 从std::thread、std::mutex、std::future和std::async等基础类的使用,到内存模型和原子操作、基于锁和无锁数据结构的构建,再扩展到并行算法、线程管理,最后还介绍了多线程代码的测试工作。 15 | 16 | 本书的附录部分还对C++11新语言特性中与多线程相关的项目进行了简要的介绍,并提供了C++11线程库的完整参考。 17 | 18 | 本书适合于需要深入了解C++多线程开发的读者,以及使用C++进行各类软件开发的开发人员、测试人员。 19 | 20 | 对于使用第三方线程库的读者,也可以从本书后面的章节中了解到相关的指引和技巧。 21 | 22 | 同时,本书还可以作为C++11线程库的参考工具书。 23 | 24 | ## 书与作者 25 | 26 | Anthony Williams是BSI C++小组的成员,拥有10多年C++应用经验。 27 | 28 | 如今多核芯处理器使用的越来越普遍。C++11标准支持多线程,这就需要程序员掌握多线程编程的原则、技术和新语言中的并发特性,确保自己处于时代前沿。 29 | 30 | 无论你的C++技术如何,本书都会指引你使用C++11写出健壮和优雅的多线程应用。本书将会探讨线程的内存模型,新的多线程库,启动线程和同步工具。在这个过程中,我们会了解并发程序中较为棘手的一些问题。 31 | 32 | 内容的大体结构: 33 | 34 | - C++11编程 35 | 36 | - 多核芯编程 37 | 38 | - 简单例子用于学习,复杂例子用于实践 39 | 40 | 本书是为C++程序员所写,同僚中可能有人对并发还没什么了解,估计也有人已经使用其他语言、API或平台写过多线程程序。不过,在看本书的时候,你们都在同一“起跑线”上。 41 | 42 | 访问本书论坛[曼宁-C++ Concurrency in Action](http://www.manning.com/williams/)可获取免费试读章节电子书。 43 | 44 | ## 本书相关 45 | 46 | - github 翻译地址:https://github.com/xiaoweiChen/Cpp_Concurrency_In_Action 47 | - gitbook 在线阅读:https://legacy.gitbook.com/book/chenxiaowei/cpp_concurrency_in_action 48 | - 极客学院在线阅读:http://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/ 49 | - 书中源码:https://github.com/bsmr-c-cpp/Cpp-Concurrency-in-Action 50 | - 学习C++11/14: http://www.bogotobogo.com/cplusplus/C11 51 | - 第二版github翻译地址:https://github.com/xiaoweiChen/CPP-Concurrency-In-Action-2ed-2019 52 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | * [前言](content/preface/preface-chinese.md) 4 | * [关于封面](content/about_cover_illustration/about_cover_illustration-chinese.md) 5 | * [关于本书](content/about_this_book/about_this_book-chinese.md) 6 | * [第1章 你好,C++的并发世界](content/chapter1/1.0-chinese.md) 7 | * [1.1 何谓并发](content/chapter1/1.1-chinese.md) 8 | * [1.2 为什么使用并发?](content/chapter1/1.2-chinese.md) 9 | * [1.3 `C++`中的并发和多线程](content/chapter1/1.3-chinese.md) 10 | * [1.4 开始入门](content/chapter1/1.4-chinese.md) 11 | * [1.5 本章总结](content/chapter1/1.5-chinese.md) 12 | * [第2章 线程管理](content/chapter2/2.0-chinese.md) 13 | * [2.1 线程管理的基础](content/chapter2/2.1-chinese.md) 14 | * [2.2 向线程函数传递参数](content/chapter2/2.2-chinese.md) 15 | * [2.3 转移线程所有权](content/chapter2/2.3-chinese.md) 16 | * [2.4 运行时决定线程数量](content/chapter2/2.4-chinese.md) 17 | * [2.5 标识线程](content/chapter2/2.5-chinese.md) 18 | * [2.6 本章总结](content/chapter2/2.6-chinese.md) 19 | * [第3章 线程间共享数据](content/chapter3/3.0-chinese.md) 20 | * [3.1 共享数据带来的问题](content/chapter3/3.1-chinese.md) 21 | * [3.2 使用互斥量保护共享数据](content/chapter3/3.2-chinese.md) 22 | * [3.3 保护共享数据的替代设施](content/chapter3/3.3-chinese.md) 23 | * [3.4 本章总结](content/chapter3/3.4-chinese.md) 24 | * [第4章 同步并发操作](content/chapter4/4.0-chinese.md) 25 | * [4.1 等待一个事件或其他条件](content/chapter4/4.1-chinese.md) 26 | * [4.2 使用期望等待一次性事件](content/chapter4/4.2-chinese.md) 27 | * [4.3 限定等待时间](content/chapter4/4.3-chinese.md) 28 | * [4.4 使用同步操作简化代码](content/chapter4/4.4-chinese.md) 29 | * [4.5 本章总结](content/chapter4/4.5-chinese.md) 30 | * [第5章 `C++`内存模型和原子类型操作](content/chapter5/5.0-chinese.md) 31 | * [5.1 内存模型基础](content/chapter5/5.1-chinese.md) 32 | * [5.2 `C++`中的原子操作和原子类型](content/chapter5/5.2-chinese.md) 33 | * [5.3 同步操作和强制排序](content/chapter5/5.3-chinese.md) 34 | * [5.4 本章总结](content/chapter5/5.4-chinese.md) 35 | * [第6章 基于锁的并发数据结构设计](content/chapter6/6.0-chinese.md) 36 | * [6.1 为并发设计的意义何在?](content/chapter6/6.1-chinese.md) 37 | * [6.2 基于锁的并发数据结构](content/chapter6/6.2-chinese.md) 38 | * [6.3 基于锁设计更加复杂的数据结构](content/chapter6/6.3-chinese.md) 39 | * [6.4 本章总结](content/chapter6/6.4-chinese.md) 40 | * [第7章 无锁并发数据结构设计](content/chapter7/7.0-chinese.md) 41 | * [7.1 定义和意义](content/chapter7/7.1-chinese.md) 42 | * [7.2 无锁数据结构的例子](content/chapter7/7.2-chinese.md) 43 | * [7.3 对于设计无锁数据结构的指导建议](content/chapter7/7.3-chinese.md) 44 | * [7.4 本章总结](content/chapter7/7.4-chinese.md) 45 | * [第8章 并发代码设计](content/chapter8/8.0-chinese.md) 46 | * [8.1 线程间划分工作的技术](content/chapter8/8.1-chinese.md) 47 | * [8.2 如何让数据紧凑?](content/chapter8/8.2-chinese.md) 48 | * [8.3 为多线程性能设计数据结构](content/chapter8/8.3-chinese.md) 49 | * [8.4 设计并发代码的注意事项](content/chapter8/8.4-chinese.md) 50 | * [8.5 在实践中设计并发代码](content/chapter8/8.5-chinese.md) 51 | * [8.6 本章总结](content/chapter8/8.6-chinese.md) 52 | * [第9章 高级线程管理](content/chapter9/9.0-chinese.md) 53 | * [9.1 线程池](content/chapter9/9.1-chinese.md) 54 | * [9.2 中断线程](content/chapter9/9.2-chinese.md) 55 | * [9.3 本章总结](content/chapter9/9.3-chinese.md) 56 | * [第10章 多线程程序的测试和调试](content/chapter10/10.0-chinese.md) 57 | * [10.1 与并发相关的错误类型](content/chapter10/10.1-chinese.md) 58 | * [10.2 定位并发错误的技术](content/chapter10/10.2-chinese.md) 59 | * [10.3 本章总结](content/chapter10/10.3-chinese.md) 60 | * [附录A `C++`11语言特性简明参考(部分)](content/appendix_A/A.0-chinese.md) 61 | * [A.1 右值引用](content/appendix_A/A.1-chinese.md) 62 | * [A.2 删除函数](content/appendix_A/A.2-chinese.md) 63 | * [A.3 默认函数](content/appendix_A/A.3-chinese.md) 64 | * [A.4 常量表达式函数](content/appendix_A/A.4-chinese.md) 65 | * [A.5 Lambda函数](content/appendix_A/A.5-chinese.md) 66 | * [A.6 变参模板](content/appendix_A/A.6-chinese.md) 67 | * [A.7 自动推导变量类型](content/appendix_A/A.7-chinese.md) 68 | * [A.8 线程本地变量](content/appendix_A/A.8-chinese.md) 69 | * [A.9 本章总结](content/appendix_A/A.9-chinese.md) 70 | * [附录B 并发库简要对比](content/appendix_B/B.0-chinese.md) 71 | * [附录C 消息传递框架与完整的ATM示例](content/appendix_C/C.0-chinese.md) 72 | * [附录D C++线程类库参考](content/appendix_D/D.0-chinese.md) 73 | * [D.1 chrono头文件](content/appendix_D/D.1-chinese.md) 74 | * [D.2 condition_variable头文件](content/appendix_D/D.2-chinese.md) 75 | * [D.3 atomic头文件](content/appendix_D/D.3-chinese.md) 76 | * [D.4 future头文件](content/appendix_D/D.4-chinese.md) 77 | * [D.5 mutex头文件](content/appendix_D/D.5-chinese.md) 78 | * [D.6 ratio头文件](content/appendix_D/D.6-chinese.md) 79 | * [D.7 thread头文件](content/appendix_D/D.7-chinese.md) 80 | * [资源](content/resources/resource.md) 81 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "《C++ Concurrency in Action》中文版", 3 | "introduction":"本书是基于C++11新标准的并发和多线程编程深度指南。", 4 | "path": { 5 | "toc": "SUMMARY.md" 6 | } 7 | } -------------------------------------------------------------------------------- /content/about_cover_illustration/about_cover_illustration-chinese.md: -------------------------------------------------------------------------------- 1 | # 封面图片介绍 2 | 3 | 本书的封面图片的标题是“日本女性的着装”(Habit of a Lady of Japan)。这张图源自Thomas Jefferys所著的《不同民族服饰的收藏》(*Collection of the Dress of Different Nations*)[1]第四卷(大概在1757年到1772年间出版)。Thomas收集的服饰包罗万象,他的绘画优美而又细腻,对欧洲戏剧服装设计产生了长达200多年的影响。服饰中包含着一个文明的过去和现在,不同时代中各个国家的习俗通过不同的服饰栩栩如生地呈现在伦敦剧院的观众面前。 4 | 5 | 从上个世纪以来,着装风格已经发生了很多变化,各个国家和区域之间巨大的差异逐渐消失。现在已经很难分辨出不同洲不同地区的人们的着装差异。或许,我们放弃了这种文化上的差异,得到的却是更加丰富多彩的个人生活——或者说是一种更加多样有趣、更快节奏的科技生活。 6 | 7 | 在各种计算机图书铺天盖地、让人难以分辨的时代,Manning出版社正是为了赞美计算机行业中的创新性和开拓性,才选用了这个重现两个世纪之前丰富多样的地域风情的图片。 8 | 9 | ---------- 10 | 11 | 【1】 《iPhone与iPad开发实战》使用了书中的另一张图片,感兴趣的同学可以去[图灵社区](http://www.ituring.com.cn/article/39923)进行试读(只免费提供第1章内容),本章翻译复制了这本书翻译的部分内容 12 | -------------------------------------------------------------------------------- /content/about_this_book/about_this_book-chinese.md: -------------------------------------------------------------------------------- 1 | # 关于这本书 2 | 3 | 本书是并发和多线程机制指导书籍(基于C++11标准)。从最基本的`std::thread std::mutex`和`std::async`的使用,到复杂的原子操作和内存模型。 4 | 5 | ## 路线图 6 | 7 | 前4章,介绍了标准库提供的各种库工具,展示了使用方法。 8 | 9 | 第5章,涵盖了底层内存模型和原子操作的实际情况,包括原子操作如何对执行顺序进行限制(这章标志着介绍部分的结束)。 10 | 11 | 第6、7章,开始讨论高级主题,如何使用基本工具去构建复杂的数据结构——第6章是基于锁的数据结构,第7章是无锁数据结构。 12 | 13 | 第8章,对设计多线程代码给了一些指导意见,覆盖了性能问题和并行算法。 14 | 15 | 第9章,线程管理——线程池,工作队列和中断操作。 16 | 17 | 第10章,测试和调试——Bug类型,定位Bug的技巧,以及如何进行测试等等。 18 | 19 | 附录,包括新的语言特性的简要描述,主要是与多线程相关的特性,以及在第4章中提到的消息传递库的实现细节和C++11线程库的完整的参考。 20 | 21 | ## 谁应该读这本书 22 | 23 | 如果你正在用C++写一个多线程程序,你应该阅读本书。如果你正在使用C++标准库中新的多线程工具,你可以从本书中得到一些指导意见。如果你正在使用其他线程库,后面章节里的建议和技术指导也很值得一看。 24 | 25 | 阅读本书需要你有较好的C++基础;虽然,关于多线程编程的知识或者经验不是必须的,不过这些经验可能有用。 26 | 27 | ### 如何使用这本书 28 | 29 | 如果从来没有写过多线程代码,我建议你从头到尾阅读本书;不过,可以跳过第5章中的较为细节的部分。第7章内容依赖于第5章中的内容,因此,如果跳过了第5章,应该保证在读第7章时,已经读过第5章。 30 | 31 | 如果没有用过C++11的工具,为了跟上这本书的进度,可以先阅读一下附录。新工具的使用在文本中已经标注出来,不过,当遇到一些没见过的工具时,可以随时回看附录。 32 | 33 | 即使有不同环境下写多线程代码的经验,开始的章节仍有必要浏览一下,这样就能清楚地知道,你所熟知的工具在新的C++标准中对应了哪些工具。如果使用原子变量去做一些底层工作,第5章必须阅读。第8章,有关C++多线程的异常和安全性的内容很值得一看。如果你对某些关键词比较感兴趣,索引和目录能够帮你快速找到相关的内容。 34 | 35 | 你可能喜欢回顾主要的章节,并用自己的方式阅读示例代码。虽然你已经了解C++线程库,但附录D还是很有用。例如,查找每个类和函数的细节。 36 | 37 | ## 代码公约和下载 38 | 39 | 为了区分普通文本,清单和正文中的中的所有代码都采用`像这样的固定宽度的字体`。许多清单都伴随着代码注释,突出显示重要的概念。在某些情况下,你可以通过页下给出的快捷链接进行查阅。 40 | 41 | 本书所有实例的源代码,可在出版商的网站上进行下载:www.manning.com/cplusplusconcurrencyinaction。 42 | 43 | ### 软件需求 44 | 45 | 使用书中的代码,可能需要一个较新的C++编译器(要支持C++11语言的特性(见附录A)),还需要C++支持标准线程库。 46 | 47 | 写本书的时候,g++是唯一实现标准线程库的编译器(尽管Microsoft Visual Studio 2011 preview中也有实现)。g++4.3发布时添加了线程库,并且在随后的发布版本中进行扩展。g++4.3也支持部分C++11语言特性,更多特性的支持在后续发布版本中也有添加。更多细节请参考g++ C++11的状态页面[1]。 48 | 49 | Microsoft Visual Studio 2010支持部分C++11特性,例如:右值引用和lambda函数,但是没有实现线程库。 50 | 51 | 我的公司Software Solutions Ltd,销售C++11标准线程库的完整实现,其可以使用在Microsoft Visual Studio 2005, Microsoft Visual Studio 2008, Microsoft Visual Studio 2010,以及各种g++版本上[2]。这个线程库也可以用来测试本书中的例子。 52 | 53 | Boost线程库[3]提供的API,以及可移植到多个平台。本书中的大多数例子将`std::`替换为`boost::`,再`#include`引用适当的头文件,就能使用Boost线程库来运行。还有部分工具还不支持(例如`std::async`)或在Boost线程库中有着不同名字(例如:`boost::unique_future`)。 54 | 55 | ## 作者在线 56 | 57 | 购买*C++ Concurrency in Action*就能访问曼宁(*Manning Publications*)的私人网络论坛,在那里可以对本书做一些评论,问一些技术问题,获得作者或其他读者的帮助。为了能够访问论坛和订阅它的内容,在浏览器地址中输入www.manning.com/CPlusPlusConcurrencyinAction后,页面将告诉你如何注册之后访问论坛,你能获得什么样的帮助,还有论坛中的一些规则。 58 | 59 | 曼宁保证为本书的读者提供互相交流,以及和作者交流的场所。虽然曼宁自愿维护本书的论坛,但不保证这样的场所不会收取任何的费用。所以,建议你可以尝试提一些有挑战性的问题给作者,免得这样的地方白白浪费。 60 | 61 | 在本书印刷时,就可以通过Internet访问作者的在线论坛和之前讨论的文字记录。 62 | 63 | ---------- 64 | 65 | 66 | 【1】GNU Compiler Collection C++0x/C++11 status page, http://gcc.gnu.org/projects/cxx0x.html. 67 | 68 | 【2】The `just::thread` implementation of the C++ Standard Thread Library, http://www.stdthread.co.uk. 69 | 70 | 【3】The Boost C++ library collection, http://www.boost.org. -------------------------------------------------------------------------------- /content/appendix_A/A.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 附录A 对`C++`11语言特性的简要介绍 2 | 3 | 新的`C++`标准,不仅带来了对并发的支持,也将其他语言的一些特性带入标准库中。在本附录中,会给出对这些新特性进行简要介绍(这些特性用在线程库中)。除了thread_local(详见A.8部分)以外,就没有与并发直接相关的内容了,但对于多线程代码来说,它们都是很重要。我已只列出有必要的部分(例如,右值引用),这样能够使代码更容易理解。由于对新特性不熟,对用到某些特性的代码理解起来会有一些困难;没关系,当对这些特性渐渐熟知后,就能很容易的理解代码。由于`C++`11的应用越来越广泛,这些特性在代码中的使用也将会变越来越普遍。 4 | 5 | 话不多说,让我们从线程库中的右值引用开始,来熟悉对象之间所有权(线程,锁等等)的转移。 -------------------------------------------------------------------------------- /content/appendix_A/A.1-chinese.md: -------------------------------------------------------------------------------- 1 | # A.1 右值引用 2 | 3 | 如果你从事过`C++`编程,你会对引用比较熟悉,`C++`的引用允许你为已经存在的对象创建一个新的名字。对新引用所做的访问和修改操作,都会影响它的原型。 4 | 5 | 例如: 6 | 7 | ``` 8 | int var=42; 9 | int& ref=var; // 创建一个var的引用 10 | ref=99; 11 | assert(var==99); // 原型的值被改变了,因为引用被赋值了 12 | ``` 13 | 14 | 目前为止,我们用过的所有引用都是左值引用——对左值的引用。lvalue这个词来自于C语言,指的是可以放在赋值表达式左边的事物——在栈上或堆上分配的命名对象,或者其他对象成员——有明确的内存地址。rvalue这个词也来源于C语言,指的是可以出现在赋值表达式右侧的对象——例如,文字常量和临时变量。因此,左值引用只能被绑定在左值上,而不是右值。 15 | 16 | 不能这样写: 17 | 18 | ``` 19 | int& i=42; // 编译失败 20 | ``` 21 | 22 | 例如,因为42是一个右值。好吧,这有些假;你可能通常使用下面的方式讲一个右值绑定到一个const左值引用上: 23 | 24 | ``` 25 | int const& i = 42; 26 | ``` 27 | 28 | 这算是钻了标准的一个空子吧。不过,这种情况我们之前也介绍过,我们通过对左值的const引用创建临时性对象,作为参数传递给函数。 29 | 30 | 其允许隐式转换,所以你可这样写: 31 | 32 | ``` 33 | void print(std::string const& s); 34 | print("hello"); //创建了临时std::string对象 35 | ``` 36 | 37 | C++11标准介绍了*右值引用*(rvalue reference),这种方式只能绑定右值,不能绑定左值,其通过两个`&&`来进行声明: 38 | 39 | ``` 40 | int&& i=42; 41 | int j=42; 42 | int&& k=j; // 编译失败 43 | ``` 44 | 45 | 因此,可以使用函数重载的方式来确定:函数有左值或右值为参数的时候,看是否能被同名且对应参数为左值或有值引用的函数所重载。 46 | 47 | 其基础就是C++11新添语义——*移动语义*(move semantics)。 48 | 49 | ## A.1.1 移动语义 50 | 51 | 右值通常都是临时的,所以可以随意修改;如果知道函数的某个参数是一个右值,就可以将其看作为一个临时存储或“窃取”内容,也不影响程序的正确性。这就意味着,比起拷贝右值参数的内容,不如移动其内容。动态数组比较大的时候,这样能节省很多内存分配,提供更多的优化空间。试想,一个函数以`std::vector`作为一个参数,就需要将其拷贝进来,而不对原始的数据做任何操作。`C++`03/98的办法是,将这个参数作为一个左值的const引用传入,然后做内部拷贝: 52 | 53 | ``` 54 | void process_copy(std::vector const& vec_) 55 | { 56 | std::vector vec(vec_); 57 | vec.push_back(42); 58 | } 59 | ``` 60 | 61 | 这就允许函数能以左值或右值的形式进行传递,不过任何情况下都是通过拷贝来完成的。如果使用右值引用版本的函数来重载这个函数,就能避免在传入右值的时候,函数会进行内部拷贝的过程,因为可以任意的对原始值进行修改: 62 | 63 | ``` 64 | void process_copy(std::vector && vec) 65 | { 66 | vec.push_back(42); 67 | } 68 | ``` 69 | 70 | 如果这个问题存在于类的构造函数中,窃取内部右值在新的实例中使用。可以参考一下清单中的例子(默认构造函数会分配很大一块内存,在析构函数中释放)。 71 | 72 | 清单A.1 使用移动构造函数的类 73 | 74 | ``` 75 | class X 76 | { 77 | private: 78 | int* data; 79 | 80 | public: 81 | X(): 82 | data(new int[1000000]) 83 | {} 84 | 85 | ~X() 86 | { 87 | delete [] data; 88 | } 89 | 90 | X(const X& other): // 1 91 | data(new int[1000000]) 92 | { 93 | std::copy(other.data,other.data+1000000,data); 94 | } 95 | 96 | X(X&& other): // 2 97 | data(other.data) 98 | { 99 | other.data=nullptr; 100 | } 101 | }; 102 | ``` 103 | 104 | 一般情况下,拷贝构造函数①都是这么定义:分配一块新内存,然后将数据拷贝进去。不过,现在有了一个新的构造函数,可以接受右值引用来获取老数据②,就是移动构造函数。在这个例子中,只是将指针拷贝到数据中,将other以空指针的形式留在了新实例中;使用右值里创建变量,就能避免了空间和时间上的多余消耗。 105 | 106 | X类(清单A.1)中的移动构造函数,仅作为一次优化;在其他例子中,有些类型的构造函数只支持移动构造函数,而不支持拷贝构造函数。例如,智能指针`std::unique_ptr<>`的非空实例中,只允许这个指针指向其对象,所以拷贝函数在这里就不能用了(如果使用拷贝函数,就会有两个`std::unique_ptr<>`指向该对象,不满足`std::unique_ptr<>`定义)。不过,移动构造函数允许对指针的所有权,在实例之间进行传递,并且允许`std::unique_ptr<>`像一个带有返回值的函数一样使用——指针的转移是通过移动,而非拷贝。 107 | 108 | 如果你已经知道,某个变量在之后就不会在用到了,这时候可以选择显式的移动,你可以使用`static_cast`将对应变量转换为右值,或者通过调用`std::move()`函数来做这件事: 109 | 110 | ``` 111 | X x1; 112 | X x2=std::move(x1); 113 | X x3=static_cast(x2); 114 | ``` 115 | 116 | 想要将参数值不通过拷贝,转化为本地变量或成员变量时,就可以使用这个办法;虽然右值引用参数绑定了右值,不过在函数内部,会当做左值来进行处理: 117 | 118 | ``` 119 | void do_stuff(X&& x_) 120 | { 121 | X a(x_); // 拷贝 122 | X b(std::move(x_)); // 移动 123 | } 124 | do_stuff(X()); // ok,右值绑定到右值引用上 125 | X x; 126 | do_stuff(x); // 错误,左值不能绑定到右值引用上 127 | ``` 128 | 129 | 移动语义在线程库中用的比较广泛,无拷贝操作对数据进行转移可以作为一种优化方式,避免对将要被销毁的变量进行额外的拷贝。在2.2节中看到,在线程中使用`std::move()`转移`std::unique_ptr<>`得到一个新实例;在2.3节中,了解了在`std:thread`的实例间使用移动语义,用来转移线程的所有权。 130 | 131 | `std::thread`、`std::unique_lock<>`、`std::future<>`、 `std::promise<>`和`std::packaged_task<>`都不能拷贝,不过这些类都有移动构造函数,能让相关资源在实例中进行传递,并且支持用一个函数将值进行返回。`std::string`和`std::vector<>`也可以拷贝,不过它们也有移动构造函数和移动赋值操作符,就是为了避免拷贝拷贝大量数据。 132 | 133 | C++标准库不会将一个对象显式的转移到另一个对象中,除非将其销毁的时候或对其赋值的时候(拷贝和移动的操作很相似)。不过,实践中移动能保证类中的所有状态保持不变,表现良好。一个`std::thread`实例可以作为移动源,转移到新(以默认构造方式)的`std::thread`实例中。还有,`std::string`可以通过移动原始数据进行构造,并且保留原始数据的状态,不过不能保证的是原始数据中该状态是否正确(根据字符串长度或字符数量决定)。 134 | 135 | ## A.1.2 右值引用和函数模板 136 | 137 | 在使用右值引用作为函数模板的参数时,与之前的用法有些不同:如果函数模板参数以右值引用作为一个模板参数,当对应位置提供左值的时候,模板会自动将其类型认定为左值引用;当提供右值的时候,会当做普通数据使用。可能有些口语化,来看几个例子吧。 138 | 139 | 考虑一下下面的函数模板: 140 | 141 | ``` 142 | template 143 | void foo(T&& t) 144 | {} 145 | ``` 146 | 147 | 随后传入一个右值,T的类型将被推导为: 148 | 149 | ``` 150 | foo(42); // foo(42) 151 | foo(3.14159); // foo<3.14159> 152 | foo(std::string()); // foo(std::string()) 153 | ``` 154 | 155 | 不过,向foo传入左值的时候,T会被推导为一个左值引用: 156 | 157 | ``` 158 | int i = 42; 159 | foo(i); // foo(i) 160 | ``` 161 | 162 | 因为函数参数声明为`T&&`,所以就是引用的引用,可以视为是原始的引用类型。那么foo()就相当于: 163 | 164 | ``` 165 | foo(); // void foo(int& t); 166 | ``` 167 | 168 | 这就允许一个函数模板可以即接受左值,又可以接受右值参数;这种方式已经被`std::thread`的构造函数所使用(2.1节和2.2节),所以能够将可调用对象移动到内部存储,而非当参数是右值的时候进行拷贝。 -------------------------------------------------------------------------------- /content/appendix_A/A.2-chinese.md: -------------------------------------------------------------------------------- 1 | # A.2 删除函数 2 | 3 | 有时让类去做拷贝是没有意义的。`std::mutex`就是一个例子——拷贝一个互斥量,意义何在?`std::unique_lock<>`是另一个例子——一个实例只能拥有一个锁;如果要复制,拷贝的那个实例也能获取相同的锁,这样`std::unique_lock<>`就没有存在的意义了。实例中转移所有权(A.1.2节)是有意义的,其并不是使用的拷贝。当然其他例子就不一一列举了。 4 | 5 | 通常为了避免进行拷贝操作,会将拷贝构造函数和拷贝赋值操作符声明为私有成员,并且不进行实现。如果对实例进行拷贝,将会引起编译错误;如果有其他成员函数或友元函数想要拷贝一个实例,那将会引起链接错误(因为缺少实现): 6 | 7 | ``` 8 | class no_copies 9 | { 10 | public: 11 | no_copies(){} 12 | private: 13 | no_copies(no_copies const&); // 无实现 14 | no_copies& operator=(no_copies const&); // 无实现 15 | }; 16 | 17 | no_copies a; 18 | no_copies b(a); // 编译错误 19 | ``` 20 | 21 | 在C++11中,委员会意识到这种情况,但是没有意识到其会带来攻击性。因此,委员会提供了更多的通用机制:可以通过添加`= delete`将一个函数声明为删除函数。 22 | 23 | no_copise类就可以写为: 24 | 25 | ``` 26 | class no_copies 27 | { 28 | public: 29 | no_copies(){} 30 | no_copies(no_copies const&) = delete; 31 | no_copies& operator=(no_copies const&) = delete; 32 | }; 33 | ``` 34 | 35 | 这样的描述要比之前的代码更加清晰。也允许编译器提供更多的错误信息描述,当成员函数想要执行拷贝操作的时候,可将连接错误转移到编译时。 36 | 37 | 拷贝构造和拷贝赋值操作删除后,需要显式写一个移动构造函数和移动赋值操作符,与`std::thread`和`std::unique_lock<>`一样,你的类是只移动的。 38 | 39 | 下面清单中的例子,就展示了一个只移动的类。 40 | 41 | 清单A.2 只移动类 42 | 43 | ``` 44 | class move_only 45 | { 46 | std::unique_ptr data; 47 | public: 48 | move_only(const move_only&) = delete; 49 | move_only(move_only&& other): 50 | data(std::move(other.data)) 51 | {} 52 | move_only& operator=(const move_only&) = delete; 53 | move_only& operator=(move_only&& other) 54 | { 55 | data=std::move(other.data); 56 | return *this; 57 | } 58 | }; 59 | 60 | move_only m1; 61 | move_only m2(m1); // 错误,拷贝构造声明为“已删除” 62 | move_only m3(std::move(m1)); // OK,找到移动构造函数 63 | ``` 64 | 65 | 只移动对象可以作为函数的参数进行传递,并且从函数中返回,不过当想要移动左值,通常需要显式的使用`std::move()`或`static_cast`。 66 | 67 | 可以为任意函数添加`= delete`说明符,添加后就说明这些函数是不能使用的。当然,还可以用于很多的地方;删除函数可以以正常的方式参与重载解析,并且如果被使用只会引起编译错误。这种方式可以用来删除特定的重载。比如,当函数以short作为参数,为了避免扩展为int类型,可以写出重载函数(以int为参数)的声明,然后添加删除说明符: 68 | 69 | ``` 70 | void foo(short); 71 | void foo(int) = delete; 72 | ``` 73 | 74 | 现在,任何向foo函数传递int类型参数都会产生一个编译错误,不过调用者可以显式的将其他类型转化为short: 75 | 76 | ``` 77 | foo(42); // 错误,int重载声明已经删除 78 | foo((short)42); // OK 79 | ``` -------------------------------------------------------------------------------- /content/appendix_A/A.3-chinese.md: -------------------------------------------------------------------------------- 1 | # A.3 默认函数 2 | 3 | 删除函数的函数可以不进行实现,默认函数就则不同:编译器会创建函数实现,通常都是“默认”实现。当然,这些函数可以直接使用(它们都会自动生成):默认构造函数,析构函数,拷贝构造函数,移动构造函数,拷贝赋值操作符和移动赋值操作符。 4 | 5 | 为什么要这样做呢?这里列出一些原因: 6 | 7 | - 改变函数的可访问性——编译器生成的默认函数通常都是声明为public(如果想让其为protected或private成员,必须自己实现)。将其声明为默认,可以让编译器来帮助你实现函数和改变访问级别。 8 | 9 | - 作为文档——编译器生成版本已经足够使用,那么显式声明就利于其他人阅读这段代码,会让代码结构看起来很清晰。 10 | 11 | - 没有单独实现的时候,编译器自动生成函数——通常默认构造函数来做这件事,如果用户没有定义构造函数,编译器将会生成一个。当需要自定一个拷贝构造函数时(假设),如果将其声明为默认,也可以获得编译器为你实现的拷贝构造函数。 12 | 13 | - 编译器生成虚析构函数。 14 | 15 | - 声明一个特殊版本的拷贝构造函数,比如:参数类型是非const引用,而不是const引用。 16 | 17 | - 利用编译生成函数的特殊性质(如果提供了对应的函数,将不会自动生成对应函数——会在后面具体讲解)。 18 | 19 | 就像删除函数是在函数后面添加`= delete`一样,默认函数需要在函数后面添加`= default`,例如: 20 | 21 | ``` 22 | class Y 23 | { 24 | private: 25 | Y() = default; // 改变访问级别 26 | public: 27 | Y(Y&) = default; // 以非const引用作为参数 28 | T& operator=(const Y&) = default; // 作为文档的形式,声明为默认函数 29 | protected: 30 | virtual ~Y() = default; // 改变访问级别,以及添加虚函数标签 31 | }; 32 | ``` 33 | 34 | 编译器生成函数都有独特的特性,这是用户定义版本所不具备的。最大的区别就是编译器生成的函数都很简单。 35 | 36 | 列出了几点重要的特性: 37 | 38 | - 对象具有简单的拷贝构造函数,拷贝赋值操作符和析构函数,都能通过memcpy或memmove进行拷贝。 39 | 40 | - 字面类型用于constexpr函数(可见A.4节),必须有简单的构造,拷贝构造和析构函数。 41 | 42 | - 类的默认构造,拷贝,拷贝赋值操作符合析构函数,也可以用在一个已有构造和析构函数(用户定义)的联合体内。 43 | 44 | - 类的简单拷贝赋值操作符可以使用`std::atomic<>`类型模板(见5.2.6节),为某种类型的值提供原子操作。 45 | 46 | 仅添加`= default`不会让函数变得简单——如果类还支持其他相关标准的函数,那这个函数就是简单的——不过,用户显式的实现就不会让这些函数变简单。 47 | 48 | 第二个区别,编译器生成函数和用户提供的函数等价,也就是类中无用户提供的构造函数可以看作为一个aggregate,并且可以通过聚合初始化函数进行初始化: 49 | 50 | ``` 51 | struct aggregate 52 | { 53 | aggregate() = default; 54 | aggregate(aggregate const&) = default; 55 | int a; 56 | double b; 57 | }; 58 | aggregate x={42,3.141}; 59 | ``` 60 | 61 | 例子中,x.a被42初始化,x.b被3.141初始化。 62 | 63 | 第三个区别,编译器生成的函数只适用于构造函数;换句话说,只适用于符合某些标准的默认构造函数。 64 | 65 | ``` 66 | struct X 67 | { 68 | int a; 69 | }; 70 | ``` 71 | 72 | 如果创建了一个X的实例(未初始化),其中int(a)将会被默认初始化。 73 | 74 | 如果对象有静态存储过程,那么a将会被初始化为0;另外,当a没赋值的时候,其不定值可能会触发未定义行为: 75 | 76 | ``` 77 | X x1; // x1.a的值不明确 78 | ``` 79 | 80 | 另外,当使用显示调用构造函数的方式对X进行初始化,a就会被初始化为0: 81 | 82 | ``` 83 | X x2 = X(); // x2.a == 0 84 | ``` 85 | 86 | 这种奇怪的属性会扩展到基础类和成员函数中。当类的默认构造函数是由编译器提供,并且一些数据成员和基类都是有编译器提供默认构造函数时,还有基类的数据成员和该类中的数据成员都是内置类型的时候,其值要不就是不确定的,要不就是被初始化为0(与默认构造函数是否能被显式调用有关)。 87 | 88 | 虽然这条规则令人困惑,并且容易造成错误,不过也很有用;当你编写构造函数的时候,就不会用到这个特性;数据成员,通常都可以被初始化(指定了一个值或调用了显式构造函数),或不会被初始化(因为不需要): 89 | 90 | ``` 91 | X::X():a(){} // a == 0 92 | X::X():a(42){} // a == 42 93 | X::X(){} // 1 94 | ``` 95 | 96 | 第三个例子中①,省略了对a的初始化,X中a就是一个未被初始化的非静态实例,初始化的X实例都会有静态存储过程。 97 | 98 | 通常的情况下,如果写了其他构造函数,编译器就不会生成默认构造函数。所以,想要自己写一个的时候,就意味着你放弃了这种奇怪的初始化特性。不过,将构造函数显示声明成默认,就能强制编译器为你生成一个默认构造函数,并且刚才说的那种特性会保留: 99 | 100 | ``` 101 | X::X() = default; // 应用默认初始化规则 102 | ``` 103 | 104 | 这种特性用于原子变量(见5.2节),默认构造函数显式为默认。初始值通常都没有定义,除非具有(a)一个静态存储的过程(静态初始化为0),(b)显式调用默认构造函数,将成员初始化为0,(c)指定一个特殊的值。注意,这种情况下的原子变量,为允许静态初始化过程,构造函数会通过一个声明为constexpr(见A.4节)的值为原子变量进行初始化。 -------------------------------------------------------------------------------- /content/appendix_A/A.4-chinese.md: -------------------------------------------------------------------------------- 1 | # A.4 常量表达式函数 2 | 3 | 整型字面值,例如42,就是常量表达式。所以,简单的数学表达式,例如,23x2-4。可以使用其来初始化const整型变量,然后将const整型变量作为新表达的一部分: 4 | 5 | ``` 6 | const int i=23; 7 | const int two_i=i*2; 8 | const int four=4; 9 | const int forty_two=two_i-four; 10 | ``` 11 | 12 | 使用常量表达式创建变量也可用在其他常量表达式中,有些事只能用常量表达式去做: 13 | 14 | - 指定数组长度: 15 | 16 | ``` 17 | int bounds=99; 18 | int array[bounds]; // 错误,bounds不是一个常量表达式 19 | const int bounds2=99; 20 | int array2[bounds2]; // 正确,bounds2是一个常量表达式 21 | ``` 22 | 23 | - 指定非类型模板参数的值: 24 | 25 | ``` 26 | template 27 | struct test 28 | {}; 29 | test ia; // 错误,bounds不是一个常量表达式 30 | test ia2; // 正确,bounds2是一个常量表达式 31 | ``` 32 | 33 | - 对类中static const整型成员变量进行初始化: 34 | 35 | ``` 36 | class X 37 | { 38 | static const int the_answer=forty_two; 39 | }; 40 | ``` 41 | 42 | - 对内置类型进行初始化或可用于静态初始化集合: 43 | 44 | ``` 45 | struct my_aggregate 46 | { 47 | int a; 48 | int b; 49 | }; 50 | static my_aggregate ma1={forty_two,123}; // 静态初始化 51 | int dummy=257; 52 | static my_aggregate ma2={dummy,dummy}; // 动态初始化 53 | ``` 54 | 55 | - 静态初始化可以避免初始化顺序和条件变量的问题。 56 | 57 | 这些都不是新添加的——你可以在1998版本的C++标准中找到对应上面实例的条款。不过,新标准中常量表达式进行了扩展,并添加了新的关键字——`constexpr`。 58 | 59 | `constexpr`会对功能进行修改,当参数和函数返回类型符合要求,并且实现很简单,那么这样的函数就能够被声明为`constexpr`,这样函数可以当做常数表达式来使用: 60 | 61 | ``` 62 | constexpr int square(int x) 63 | { 64 | return x*x; 65 | } 66 | int array[square(5)]; 67 | ``` 68 | 69 | 在这个例子中,array有25个元素,因为square函数的声明为`constexpr`。当然,这种方式可以当做常数表达式来使用,不意味着什么情况下都是能够自动转换为常数表达式: 70 | 71 | ``` 72 | int dummy=4; 73 | int array[square(dummy)]; // 错误,dummy不是常数表达式 74 | ``` 75 | 76 | dummy不是常数表达式,所以square(dummy)也不是——就是一个普通函数调用——所以其不能用来指定array的长度。 77 | 78 | ## A.4.1 常量表达式和自定义类型 79 | 80 | 目前为止的例子都是以内置int型展开的。不过,在新C++标准库中,对于满足字面类型要求的任何类型,都可以用常量表达式来表示。 81 | 82 | 要想划分到字面类型中,需要满足一下几点: 83 | 84 | - 一般的拷贝构造函数。 85 | 86 | - 一般的析构函数。 87 | 88 | - 所有成员变量都是非静态的,且基类需要是一般类型。 89 | 90 | - 必须具有一个一般的默认构造函数,或一个constexpr构造函数。 91 | 92 | 后面会了解一下constexpr构造函数。 93 | 94 | 现在,先将注意力集中在默认构造函数上,就像下面清单中的CX类一样。 95 | 96 | 清单A.3(一般)默认构造函数的类 97 | 98 | ``` 99 | class CX 100 | { 101 | private: 102 | int a; 103 | int b; 104 | public: 105 | CX() = default; // 1 106 | CX(int a_, int b_): // 2 107 | a(a_),b(b_) 108 | {} 109 | int get_a() const 110 | { 111 | return a; 112 | } 113 | int get_b() const 114 | { 115 | return b; 116 | } 117 | int foo() const 118 | { 119 | return a+b; 120 | } 121 | }; 122 | ``` 123 | 124 | 注意,这里显式的声明了默认构造函数①(见A.3节),为了保存用户定义的构造函数②。因此,这种类型符合字面类型的要求,可以将其用在常量表达式中。 125 | 126 | 可以提供一个constexpr函数来创建一个实例,例如: 127 | 128 | ``` 129 | constexpr CX create_cx() 130 | { 131 | return CX(); 132 | } 133 | ``` 134 | 135 | 也可以创建一个简单的constexpr函数来拷贝参数: 136 | 137 | ``` 138 | constexpr CX clone(CX val) 139 | { 140 | return val; 141 | } 142 | ``` 143 | 144 | 不过,constexpr函数只有其他constexpr函数可以进行调用。CX类中声明成员函数和构造函数为constexpr: 145 | 146 | ``` 147 | class CX 148 | { 149 | private: 150 | int a; 151 | int b; 152 | public: 153 | CX() = default; 154 | constexpr CX(int a_, int b_): 155 | a(a_),b(b_) 156 | {} 157 | constexpr int get_a() const // 1 158 | { 159 | return a; 160 | } 161 | constexpr int get_b() // 2 162 | { 163 | return b; 164 | } 165 | constexpr int foo() 166 | { 167 | return a+b; 168 | } 169 | }; 170 | ``` 171 | 172 | 注意,const对于get_a()①来说就是多余的,因为在使用constexpr时就为const了,所以const描述符在这里会被忽略。 173 | 174 | 这就允许更多复杂的constexpr函数存在: 175 | 176 | ``` 177 | constexpr CX make_cx(int a) 178 | { 179 | return CX(a,1); 180 | } 181 | constexpr CX half_double(CX old) 182 | { 183 | return CX(old.get_a()/2,old.get_b()*2); 184 | } 185 | constexpr int foo_squared(CX val) 186 | { 187 | return square(val.foo()); 188 | } 189 | int array[foo_squared(half_double(make_cx(10)))]; // 49个元素 190 | ``` 191 | 192 | 函数都很有趣,如果想要计算数组的长度或一个整型常量,就需要使用这种方式。最大的好处是常量表达式和constexpr函数会设计到用户定义类型的对象,可以使用这些函数对这些对象进行初始化。因为常量表达式的初始化过程是静态初始化,所以就能避免条件竞争和初始化顺序的问题: 193 | 194 | ``` 195 | CX si=half_double(CX(42,19)); // 静态初始化 196 | ``` 197 | 198 | 当构造函数被声明为constexpr,且构造函数参数是常量表达式时,那么初始化过程就是常数初始化(可能作为静态初始化的一部分)。随着并发的发展,C++11标准中有一个重要的改变:允许用户定义构造函数进行静态初始化,就可以在初始化的时候避免条件竞争,因为静态过程能保证初始化过程在代码运行前进行。 199 | 200 | 特别是关于`std::mutex`(见3.2.1节)或`std::atomic<>`(见5.2.6节),当想要使用一个全局实例来同步其他变量的访问时,同步访问就能避免条件竞争的发生。构造函数中,互斥量不可能产生条件竞争,因此对于`std::mutex`的默认构造函数应该被声明为constexpr,为了保证互斥量初始化过程是一个静态初始化过程的一部分。 201 | 202 | ## A.4.2 常量表达式对象 203 | 204 | 目前,已经了解了constexpr在函数上的应用。constexpr也可以用在对象上,主要是用来做判断的;验证对象是否是使用常量表达式,constexpr构造函数或组合常量表达式进行初始化。 205 | 206 | 且这个对象需要声明为const: 207 | 208 | ``` 209 | constexpr int i=45; // ok 210 | constexpr std::string s(“hello”); // 错误,std::string不是字面类型 211 | 212 | int foo(); 213 | constexpr int j=foo(); // 错误,foo()没有声明为constexpr 214 | ``` 215 | 216 | ## A.4.3 常量表达式函数的要求 217 | 218 | 将一个函数声明为constexpr,也是有几点要求的;当不满足这些要求,constexpr声明将会报编译错误。 219 | 220 | - 所有参数都必须是字面类型。 221 | 222 | - 返回类型必须是字面类型。 223 | 224 | - 函数体内必须有一个return。 225 | 226 | - return的表达式需要满足常量表达式的要求。 227 | 228 | - 构造返回值/表达式的任何构造函数或转换操作,都需要是constexpr。 229 | 230 | 看起来很简单,要在内联函数中使用到常量表达式,返回的还是个常量表达式,还不能对任何东西进行改动。constexpr函数就是无害的纯洁的函数。 231 | 232 | constexpr类成员函数,需要追加几点要求: 233 | 234 | - constexpr成员函数不能是虚函数。 235 | 236 | - 对应类必须有字面类的成员。 237 | 238 | constexpr构造函数的规则也有些不同: 239 | 240 | - 构造函数体必须为空。 241 | 242 | - 每一个基类必须可初始化。 243 | 244 | - 每个非静态数据成员都需要初始化。 245 | 246 | - 初始化列表的任何表达式,必须是常量表达式。 247 | 248 | - 构造函数可选择要进行初始化的数据成员,并且基类必须有constexpr构造函数。 249 | 250 | - 任何用于构建数据成员的构造函数和转换操作,以及和初始化表达式相关的基类必须为constexpr。 251 | 252 | 这些条件同样适用于成员函数,除非函数没有返回值,也就没有return语句。 253 | 254 | 另外,构造函数对初始化列表中的所有基类和数据成员进行初始化。一般的拷贝构造函数会隐式的声明为constexpr。 255 | 256 | ## A.4.4 常量表达式和模板 257 | 258 | 将constexpr应用于函数模板,或一个类模板的成员函数;根据参数,如果模板的返回类型不是字面类,编译器会忽略其常量表达式的声明。当模板参数类型合适,且为一般inline函数,就可以将类型写成constexpr类型的函数模板。 259 | 260 | ``` 261 | template 262 | constexpr T sum(T a,T b) 263 | { 264 | return a+b; 265 | } 266 | constexpr int i=sum(3,42); // ok,sum是constexpr 267 | std::string s= 268 | sum(std::string("hello"), 269 | std::string(" world")); // 也行,不过sum就不是constexpr了 270 | ``` 271 | 272 | 函数需要满足所有constexpr函数所需的条件。不能用多个constexpr来声明一个函数,因为其是一个模板;这样也会带来一些编译错误。 273 | -------------------------------------------------------------------------------- /content/appendix_A/A.5-chinese.md: -------------------------------------------------------------------------------- 1 | # A.5 Lambda函数 2 | 3 | lambda函数在`C++`11中的加入很是令人兴奋,因为lambda函数能够大大简化代码复杂度(语法糖:利于理解具体的功能),避免实现调用对象。`C++`11的lambda函数语法允许在需要使用的时候进行定义。能为等待函数,例如`std::condition_variable`(如同4.1.1节中的例子)提供很好谓词函数,其语义可以用来快速的表示可访问的变量,而非使用类中函数来对成员变量进行捕获。 4 | 5 | 最简单的情况下,lambda表达式就一个自给自足的函数,不需要传入函数仅依赖管局变量和函数,甚至都可以不用返回一个值。这样的lambda表达式的一系列语义都需要封闭在括号中,还要以方括号作为前缀: 6 | 7 | ``` 8 | []{ // lambda表达式以[]开始 9 | do_stuff(); 10 | do_more_stuff(); 11 | }(); // 表达式结束,可以直接调用 12 | ``` 13 | 14 | 例子中,lambda表达式通过后面的括号调用,不过这种方式不常用。一方面,如果想要直接调用,可以在写完对应的语句后,就对函数进行调用。对于函数模板,传递一个参数进去时很常见的事情,甚至可以将可调用对象作为其参数传入;可调用对象通常也需要一些参数,或返回一个值,亦或两者都有。如果想给lambda函数传递参数,可以参考下面的lambda函数,其使用起来就像是一个普通函数。例如,下面代码是将vector中的元素使用`std::cout`进行打印: 15 | 16 | ``` 17 | std::vector data=make_data(); 18 | std::for_each(data.begin(),data.end(),[](int i){std::cout< lk(m); 32 | cond.wait(lk,[]{return data_ready;}); // 1 33 | } 34 | ``` 35 | 36 | lambda的返回值传递给cond.wait()①,函数就能推断出data_ready的类型是bool。当条件变量从等待中苏醒后,上锁阶段会调用lambda函数,并且当data_ready为true时,仅返回到wait()中。 37 | 38 | 当lambda函数体中有多个return语句,就需要显式的指定返回类型。只有一个返回语句的时候,也可以这样做,不过这样可能会让你的lambda函数体看起来更复杂。返回类型可以使用跟在参数列表后面的箭头(->)进行设置。如果lambda函数没有任何参数,还需要包含(空)的参数列表,这样做是为了能显式的对返回类型进行指定。对条件变量的预测可以写成下面这种方式: 39 | 40 | ``` 41 | cond.wait(lk,[]()->bool{return data_ready;}); 42 | ``` 43 | 44 | 还可以对lambda函数进行扩展,比如:加上log信息的打印,或做更加复杂的操作: 45 | 46 | ``` 47 | cond.wait(lk,[]()->bool{ 48 | if(data_ready) 49 | { 50 | std::cout<<”Data ready”< make_offseter(int offset) 71 | { 72 | return [=](int j){return offset+j;}; 73 | } 74 | ``` 75 | 76 | 当调用make_offseter时,就会通过`std::function<>`函数包装返回一个新的lambda函数体。 77 | 78 | 这个带有返回的函数添加了对参数的偏移功能。例如: 79 | 80 | ``` 81 | int main() 82 | { 83 | std::function offset_42=make_offseter(42); 84 | std::function offset_123=make_offseter(123); 85 | std::cout< offset_a=[&](int j){return offset+j;}; // 2 101 | offset=123; // 3 102 | std::function offset_b=[&](int j){return offset+j;}; // 4 103 | std::cout< f=[=,&j,&k]{return i+j+k;}; 120 | i=1; 121 | j=2; 122 | k=3; 123 | std::cout< f=[&,j,k]{return i+j+k;}; 135 | i=1; 136 | j=2; 137 | k=3; 138 | std::cout< f=[&i,j,&k]{return i+j+k;}; 149 | i=1; 150 | j=2; 151 | k=3; 152 | std::cout<& vec) 163 | { 164 | std::for_each(vec.begin(),vec.end(), 165 | [this](int& i){i+=some_data;}); 166 | } 167 | }; 168 | ``` 169 | 170 | 并发的上下文中,lambda是很有用的,其可以作为谓词放在`std::condition_variable::wait()`(见4.1.1节)和`std::packaged_task<>`(见4.2.1节)中;或是用在线程池中,对小任务进行打包。也可以线程函数的方式`std::thread`的构造函数(见2.1.1),以及作为一个并行算法实现,在parallel_for_each()(见8.5.1节)中使用。 171 | -------------------------------------------------------------------------------- /content/appendix_A/A.6-chinese.md: -------------------------------------------------------------------------------- 1 | # A.6 变参模板 2 | 3 | 变参模板:就是可以使用不定数量的参数进行特化的模板。就像你接触到的变参函数一样,printf就接受可变参数。现在,就可以给你的模板指定不定数量的参数了。变参模板在整个`C++`线程库中都有使用,例如:`std::thread`的构造函数就是一个变参类模板。从使用者的角度看,仅知道模板可以接受无限个参数就够了,不过当要写这么一个模板或对其工作原理很感兴趣时,就需要了解一些细节。 4 | 5 | 和变参函数一样,变参部分可以在参数列表章使用省略号`...`代表,变参模板需要在参数列表中使用省略号: 6 | 7 | ``` 8 | template 9 | class my_template 10 | {}; 11 | ``` 12 | 13 | 即使主模板不是变参模板,模板进行部分特化的类中,也可以使用可变参数模板。例如,`std::packaged_task<>`(见4.2.1节)的主模板就是一个简单的模板,这个简单的模板只有一个参数: 14 | 15 | ``` 16 | template 17 | class packaged_task; 18 | ``` 19 | 20 | 不过,并不是所有地方都这样定义;对于部分特化模板来说,其就像是一个“占位符”: 21 | 22 | ``` 23 | template 24 | class packaged_task; 25 | ``` 26 | 27 | 部分特化的类就包含实际定义的类;在第4章,可以写一个`std::packaged_task`来声明一个以`std::string`和double作为参数的任务,当执行这个任务后结果会由`std::future`进行保存。 28 | 29 | 声明展示了两个变参模板的附加特性。第一个比较简单:普通模板参数(例如ReturnType)和可变模板参数(Args)可以同时声明。第二个特性,展示了`Args...`特化类的模板参数列表中如何使用,为了展示实例化模板中的Args的组成类型。实际上,因为这是部分特化,所以其作为一种模式进行匹配;在列表中出现的类型(被Args捕获)都会进行实例化。参数包(parameter pack)调用可变参数Args,并且使用`Args...`作为包的扩展。 30 | 31 | 和可变参函数一样,变参部分可能什么都没有,也可能有很多类型项。例如,`std::packaged_task`中ReturnType参数就是my_class,并且Args参数包是空的,不过`std::packaged_task`中,ReturnType为void,并且Args列表中的类型就有:int, double, my_class&和std::string*。 32 | 33 | ## A.6.1 扩展参数包 34 | 35 | 变参模板主要依靠包括扩展功能,因为不能限制有更多的类型添加到模板参数中。首先,列表中的参数类型使用到的时候,可以使用包扩展,比如:需要给其他模板提供类型参数。 36 | 37 | ``` 38 | template 39 | struct dummy 40 | { 41 | std::tuple data; 42 | }; 43 | ``` 44 | 45 | 成员变量data是一个`std::tuple<>`实例,包含所有指定类型,所以dummy的成员变量就为`std::tuple`。 46 | 47 | 可以将包扩展和普通类型相结合: 48 | 49 | ``` 50 | template 51 | struct dummy2 52 | { 53 | std::tuple data; 54 | }; 55 | ``` 56 | 57 | 这次,元组中添加了额外的(第一个)成员类型`std::string`。其优雅指出在于,可以通过包扩展的方式创建一种模式,这种模式会在之后将每个元素拷贝到扩展之中,可以使用`...`来表示扩展模式的结束。 58 | 59 | 例如,创建使用参数包来创建元组中所有的元素,不如在元组中创建指针,或使用`std::unique_ptr<>`指针,指向对应元素: 60 | 61 | ``` 62 | template 63 | struct dummy3 64 | { 65 | std::tuple pointers; 66 | std::tuple ...> unique_pointers; 67 | }; 68 | ``` 69 | 70 | 类型表达式会比较复杂,提供的参数包是在类型表达式中产生,并且表达式中使用`...`作为扩展。当参数包已经扩展 ,包中的每一项都会代替对应的类型表达式,在结果列表中产生相应的数据项。因此,当参数包Params包含int,int,char类型,那么`std::tuple,double> ... >`将扩展为`std::tuple,double>`,`std::pair,double>`,`std::pair, double> >`。如果包扩展被当做模板参数列表使用,那么模板就不需要变长的参数了;如果不需要了,参数包就要对模板参数的要求进行准确的匹配: 71 | 72 | ``` 73 | template 74 | struct dummy4 75 | { 76 | std::pair data; 77 | }; 78 | dummy4 a; // 1 ok,为std::pair 79 | dummy4 b; // 2 错误,无第二个类型 80 | dummy4 c; // 3 错误,类型太多 81 | ``` 82 | 83 | 可以使用包扩展的方式,对函数的参数进行声明: 84 | 85 | ``` 86 | template 87 | void foo(Args ... args); 88 | ``` 89 | 90 | 这将会创建一个新参数包args,其是一组函数参数,而非一组类型,并且这里`...`也能像之前一样进行扩展。例如,可以在`std::thread`的构造函数中使用,使用右值引用的方式获取函数所有的参数(见A.1节): 91 | 92 | ``` 93 | template 94 | thread::thread(CallableType&& func,Args&& ... args); 95 | ``` 96 | 97 | 函数参数包也可以用来调用其他函数,将制定包扩展成参数列表,匹配调用的函数。如同类型扩展一样,也可以使用某种模式对参数列表进行扩展。 98 | 99 | 例如,使用`std::forward()`以右值引用的方式来保存提供给函数的参数: 100 | 101 | ``` 102 | template 103 | void bar(ArgTypes&& ... args) 104 | { 105 | foo(std::forward(args)...); 106 | } 107 | ``` 108 | 109 | 注意一下这个例子,包扩展包括对类型包ArgTypes和函数参数包args的扩展,并且省略了其余的表达式。 110 | 111 | 当这样调用bar函数: 112 | 113 | ``` 114 | int i; 115 | bar(i,3.141,std::string("hello ")); 116 | ``` 117 | 118 | 将会扩展为 119 | 120 | ``` 121 | template<> 122 | void bar( 123 | int& args_1, 124 | double&& args_2, 125 | std::string&& args_3) 126 | { 127 | foo(std::forward(args_1), 128 | std::forward(args_2), 129 | std::forward(args_3)); 130 | } 131 | ``` 132 | 133 | 这样就将第一个参数以左值引用的形式,正确的传递给了foo函数,其他两个函数都是以右值引用的方式传入的。 134 | 135 | 最后一件事,参数包中使用`sizeof...`操作可以获取类型参数类型的大小,`sizeof...(p)`就是p参数包中所包含元素的个数。不管是类型参数包或函数参数包,结果都是一样的。这可能是唯一一次在使用参数包的时候,没有加省略号;这里的省略号是作为`sizeof...`操作的一部分,所以不算是用到省略号。 136 | 137 | 下面的函数会返回参数的数量: 138 | 139 | ``` 140 | template 141 | unsigned count_args(Args ... args) 142 | { 143 | return sizeof... (Args); 144 | } 145 | ``` 146 | 147 | 就像普通的sizeof操作一样,`sizeof...`的结果为常量表达式,所以其可以用来指定定义数组长度,等等。 -------------------------------------------------------------------------------- /content/appendix_A/A.7-chinese.md: -------------------------------------------------------------------------------- 1 | # A.7 自动推导变量类型 2 | 3 | `C++`是静态语言:所有变量的类型,都会在编译时被准确指定。所以,作为程序员你需要为每个变量指定对应的类型。 4 | 5 | 有些时候就需要使用一些繁琐类型定义,比如: 6 | 7 | ``` 8 | std::map> m; 9 | std::map>::iterator 10 | iter=m.find("my key"); 11 | ``` 12 | 13 | 常规的解决办法是使用typedef来缩短类型名的长度。这种方式在`C++`11中仍然可行,不过这里要介绍一种新的解决办法:如果一个变量需要通过一个已初始化的变量类型来为其做声明,那么就可以直接使用`auto`关键字。这样,编译器就会通过已初始化的变量,去自动推断变量的类型。 14 | 15 | ``` 16 | auto iter=m.find("my key"); 17 | ``` 18 | 19 | 当然,`auto`还有很多种用法:可以使用它来声明const、指针或引用变量。这里使用`auto`对相关类型进行了声明: 20 | 21 | ``` 22 | auto i=42; // int 23 | auto& j=i; // int& 24 | auto const k=i; // int const 25 | auto* const p=&i; // int * const 26 | ``` 27 | 28 | 变量类型的推导规则是建立一些语言规则基础上:函数模板参数。其声明形式如下: 29 | 30 | ``` 31 | some-type-expression-involving-auto var=some-expression; 32 | ``` 33 | 34 | var变量的类型与声明函数模板的参数的类型相同。要想替换`auto`,需要使用完整的类型参数: 35 | 36 | ``` 37 | template 38 | void f(type-expression var); 39 | f(some-expression); 40 | ``` 41 | 42 | 在使用`auto`的时候,数组类型将衰变为指针,引用将会被删除(除非将类型进行显式为引用),比如: 43 | 44 | ``` 45 | int some_array[45]; 46 | auto p=some_array; // int* 47 | int& r=*p; 48 | auto x=r; // int 49 | auto& y=r; // int& 50 | ``` 51 | 52 | 这样能大大简化变量的声明过程,特别是在类型标识符特别长,或不清楚具体类型的时候(例如,调用函数模板,等到的目标值类型就是不确定的)。 -------------------------------------------------------------------------------- /content/appendix_A/A.8-chinese.md: -------------------------------------------------------------------------------- 1 | # A.8 线程本地变量 2 | 3 | 线程本地变量允许程序中的每个线程都有一个独立的实例拷贝。可以使用`thread_local`关键字来对这样的变量进行声明。命名空间内的变量,静态成员变量,以及本地变量都可以声明成线程本地变量,为了在线程运行前对这些数据进行存储操作: 4 | 5 | ``` 6 | thread_local int x; // 命名空间内的线程本地变量 7 | 8 | class X 9 | { 10 | static thread_local std::string s; // 线程本地的静态成员变量 11 | }; 12 | static thread_local std::string X::s; // 这里需要添加X::s 13 | 14 | void foo() 15 | { 16 | thread_local std::vector v; // 一般线程本地变量 17 | } 18 | ``` 19 | 20 | 由命名空间或静态数据成员构成的线程本地变量,需要在线程单元对其进行使用**前**进行构建。有些实现中,会将对线程本地变量的初始化过程,放在线程中去做;还有一些可能会在其他时间点做初始化,在一些有依赖的组合中,根据具体情况来进行决定。将没有构造好的线程本地变量传递给线程单元使用,不能保证它们会在线程中进行构造。这样就可以动态加载带有线程本地变量的模块——变量首先需要在一个给定的线程中进行构造,之后其他线程就可以通过动态加载模块对线程本地变量进行引用。 21 | 22 | 函数中声明的线程本地变量,需要使用一个给定线程进行初始化(通过第一波控制流将这些声明传递给指定线程)。如果函数没有被指定线程调用,那么这个函数中声明的线程本地变量就不会构造。本地静态变量也是同样的情况,除非其单独的应用于每一个线程。 23 | 24 | 静态变量与线程本地变量会共享一些属性——它们可以做进一步的初始化(比如,动态初始化);如果在构造线程本地变量时抛出异常,`srd::terminate()`就会将程序终止。 25 | 26 | 析构函数会在构造线程本地变量的那个线程返回时调用,析构顺序是构造的逆顺序。当初始化顺序没有指定时,确定析构函数和这些变量是否有相互依存关系就尤为重要了。当线程本地变量的析构函数抛出异常时,`std::terminate()`会被调用,将程序终止。 27 | 28 | 当线程调用`std::exit()`或从main()函数返回(等价于调用`std::exit()`作为main()的“返回值”)时,线程本地变量也会为了这个线程进行销毁。应用退出时还有线程在运行,对于这些线程来说,线程本地变量的析构函数就没有被调用。 29 | 30 | 虽然,线程本地变量在不同线程上有不同的地址,不过还是可以获取指向这些变量的一般指针。指针会在线程中,通过获取地址的方式,引用相应的对象。当引用被销毁的对象时,会出现未定义行为,所以在向其他线程传递线程本地变量指针时,就需要保证指向对象所在的线程结束后,不能对相应的指针进行解引用。 -------------------------------------------------------------------------------- /content/appendix_A/A.9-chinese.md: -------------------------------------------------------------------------------- 1 | # A.9 本章总结 2 | 3 | 本附录仅是摘录了部分`C++`11标准的新特性,因为这些特性和线程库之间有着良好的互动。其他的新特性,包括:静态断言(static_assert),强类型枚举(enum class),委托构造函数,Unicode码支持,模板别名,以及统一的初始化序列。对于新功能的详细描述已经超出了本书的范围;需要另外一本书来进行详细介绍。对标准改动的最好的概述可能就是由Bjarne Stroustrup编写的《`C++`11FAQ》[1], 其他`C++`的参考书籍也会在未来对`C++`11标准进行覆盖。 4 | 5 | 希望这里的简短介绍,能让你了解这些新功能和线程库之间的关系,并且在写多线程代码的时候能用到这些新功能。虽然,本附录为了新特性提供了足够简单的例子,不过这里还是一个简单的介绍,并非新功能的一份完整的参考或教程。如果想在你的代码中大量使用这些新功能,我建议去找相关权威的参考书或教程,了解更加详细的情况。 6 | 7 | ---------- 8 | 9 | 【1】 http://www.research.att.com/~bs/C++0xFAQ.html -------------------------------------------------------------------------------- /content/appendix_B/B.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 附录B 并发库的简单比较 2 | 3 | 虽然,C++11才开始正式支持并发,不过,高级编程语言都支持并发和多线程已经不是什么新鲜事了。例如,Java在第一个发布版本中就支持多线程编程,在某些平台上也提供符合POSIX C标准的多线程接口,还有[Erlang](http://www.erlang.org/)支持消息的同步传递(有点类似于MPI)。当然还有使用C++类的库,比如Boost,其将底层多线程接口进行包装,适用于任何给定的平台(不论是使用POSIX C的接口,或其他接口),其对支持的平台会提供可移植接口。 4 | 5 | 这些库或者编程语言,已经写了很多多线程应用,并且在使用这些库写多线程代码的经验,可以借鉴到C++中,本附录就对Java,POSIX C,使用Boost线程库的C++,以及C++11中的多线程工具进行简单的比较,当然也会交叉引用本书的相关章节。 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |
特性 启动线程 互斥量 监视/等待谓词 原子操作和并发感知内存模型 线程安全容器 Futures(期望) 线程池 线程中断
章节引用第2章第3章第4章第5章第6章和第7章第4章第9章第9章
C++11 std::thread和其成员函数 std::mutex类和其成员函数 std::condition_variable std::atomic_xxx类型 N/A std::future<> N/A N/A
std::lock_guard<>模板 std::condition_variable_any类和其成员函数 std::atomic<>类模板 std::shared_future<>
std::unique_lock<>模板 std::atomic_thread_fence()函数 std::atomic_future<>类模板
Boost线程库 boost::thread类和成员函数 boost::mutex类和其成员函数 boost::condition_variable类和其成员函数 N/A N/A boost::unique_future<>类模板 N/A boost::thread类的interrupt()成员函数
boost::lock_guard<>类模板 boost::condition_variable_any类和其成员函数 boost::shared_future<>类模板
boost::unique_lock<>类模板
POSIX C pthread_t类型相关的API函数 pthread_mutex_t类型相关的API函数 pthread_cond_t类型相关的API函数 N/A N/A N/A N/A pthread_cancel()
pthread_create() pthread_mutex_lock() pthread_cond_wait()
pthread_detach() pthread_mutex_unlock() pthread_cond_timed_wait()
pthread_join() 等等 等等
Java java.lang.thread类 synchronized块 java.lang.Object类的wait()和notify()函数,用在内部synchronized块中 java.util.concurrent.atomic包中的volatile类型变量 java.util.concurrent包中的容器 与java.util.concurrent.future接口相关的类 java.util.concurrent.ThreadPoolExecutor类 java.lang.Thread类的interrupt()函数
-------------------------------------------------------------------------------- /content/appendix_D/D.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 附录D C++线程库参考 -------------------------------------------------------------------------------- /content/appendix_D/D.6-chinese.md: -------------------------------------------------------------------------------- 1 | # D.6 <ratio>头文件 2 | 3 | ``头文件提供在编译时进行的计算。 4 | 5 | **头文件内容** 6 | 7 | ``` 8 | namespace std 9 | { 10 | template 11 | class ratio; 12 | 13 | // ratio arithmetic 14 | template 15 | using ratio_add = see description; 16 | 17 | template 18 | using ratio_subtract = see description; 19 | 20 | template 21 | using ratio_multiply = see description; 22 | 23 | template 24 | using ratio_divide = see description; 25 | 26 | // ratio comparison 27 | template 28 | struct ratio_equal; 29 | 30 | template 31 | struct ratio_not_equal; 32 | 33 | template 34 | struct ratio_less; 35 | 36 | template 37 | struct ratio_less_equal; 38 | 39 | template 40 | struct ratio_greater; 41 | 42 | template 43 | struct ratio_greater_equal; 44 | 45 | typedef ratio<1, 1000000000000000000> atto; 46 | typedef ratio<1, 1000000000000000> femto; 47 | typedef ratio<1, 1000000000000> pico; 48 | typedef ratio<1, 1000000000> nano; 49 | typedef ratio<1, 1000000> micro; 50 | typedef ratio<1, 1000> milli; 51 | typedef ratio<1, 100> centi; 52 | typedef ratio<1, 10> deci; 53 | typedef ratio<10, 1> deca; 54 | typedef ratio<100, 1> hecto; 55 | typedef ratio<1000, 1> kilo; 56 | typedef ratio<1000000, 1> mega; 57 | typedef ratio<1000000000, 1> giga; 58 | typedef ratio<1000000000000, 1> tera; 59 | typedef ratio<1000000000000000, 1> peta; 60 | typedef ratio<1000000000000000000, 1> exa; 61 | } 62 | ``` 63 | 64 | ##D.6.1 std::ratio类型模板 65 | 66 | `std::ratio`类型模板提供了一种对在编译时进行计算的机制,通过调用合理的数,例如:半(`std::ratio<1,2>`),2/3(std::ratio<2, 3>)或15/43(std::ratio<15, 43>)。其使用在C++标准库内部,用于初始化`std::chrono::duration`类型模板。 67 | 68 | **类型定义** 69 | 70 | ``` 71 | template 72 | class ratio 73 | { 74 | public: 75 | typedef ratio type; 76 | static constexpr intmax_t num= see below; 77 | static constexpr intmax_t den= see below; 78 | }; 79 | ``` 80 | 81 | **要求**
82 | D不能为0。 83 | 84 | **描述**
85 | num和den分别为分子和分母,构造分数N/D。den总是正数。当N和D的符号相同,那么num为正数;否则num为负数。 86 | 87 | **例子** 88 | 89 | ``` 90 | ratio<4,6>::num == 2 91 | ratio<4,6>::den == 3 92 | ratio<4,-6>::num == -2 93 | ratio<4,-6>::den == 3 94 | ``` 95 | 96 | ## D.6.2 std::ratio_add模板别名 97 | 98 | `std::ratio_add`模板别名提供了两个`std::ratio`在编译时相加的机制(使用有理计算)。 99 | 100 | **定义** 101 | 102 | ``` 103 | template 104 | using ratio_add = std::ratio; 105 | ``` 106 | 107 | **先决条件**
108 | R1和R2必须使用`std::ratio`进行初始化。 109 | 110 | **效果**
111 | ratio_add被定义为一个别名,如果两数可以计算,且无溢出,该类型可以表示两个`std::ratio`对象R1和R2的和。如果计算出来的结果溢出了,那么程序里面就有问题了。在算术溢出的情况下,`std::ratio_add`应该应该与`std::ratio`相同。 112 | 113 | **例子** 114 | 115 | ``` 116 | std::ratio_add, std::ratio<2,5> >::num == 11 117 | std::ratio_add, std::ratio<2,5> >::den == 15 118 | 119 | std::ratio_add, std::ratio<7,6> >::num == 3 120 | std::ratio_add, std::ratio<7,6> >::den == 2 121 | ``` 122 | 123 | ## D.6.3 std::ratio_subtract模板别名 124 | 125 | `std::ratio_subtract`模板别名提供两个`std::ratio`数在编译时进行相减(使用有理计算)。 126 | 127 | **定义** 128 | 129 | ``` 130 | template 131 | using ratio_subtract = std::ratio; 132 | ``` 133 | 134 | **先决条件**
135 | R1和R2必须使用`std::ratio`进行初始化。 136 | 137 | **效果**
138 | ratio_add被定义为一个别名,如果两数可以计算,且无溢出,该类型可以表示两个`std::ratio`对象R1和R2的和。如果计算出来的结果溢出了,那么程序里面就有问题了。在算术溢出的情况下,`std::ratio_subtract`应该应该与`std::ratio`相同。 139 | 140 | **例子** 141 | 142 | ``` 143 | std::ratio_subtract, std::ratio<1,5> >::num == 2 144 | std::ratio_subtract, std::ratio<1,5> >::den == 15 145 | 146 | std::ratio_subtract, std::ratio<7,6> >::num == -5 147 | std::ratio_subtract, std::ratio<7,6> >::den == 6 148 | ``` 149 | 150 | ## D.6.4 std::ratio_multiply模板别名 151 | 152 | `std::ratio_multiply`模板别名提供两个`std::ratio`数在编译时进行相乘(使用有理计算)。 153 | 154 | **定义** 155 | 156 | ``` 157 | template 158 | using ratio_multiply = std::ratio; 159 | ``` 160 | 161 | **先决条件**
162 | R1和R2必须使用`std::ratio`进行初始化。 163 | 164 | **效果**
165 | ratio_add被定义为一个别名,如果两数可以计算,且无溢出,该类型可以表示两个`std::ratio`对象R1和R2的和。如果计算出来的结果溢出了,那么程序里面就有问题了。在算术溢出的情况下,`std::ratio_multiply`应该应该与`std::ratio`相同。 166 | 167 | **例子** 168 | 169 | ``` 170 | std::ratio_multiply, std::ratio<2,5> >::num == 2 171 | std::ratio_multiply, std::ratio<2,5> >::den == 15 172 | 173 | std::ratio_multiply, std::ratio<15,7> >::num == 5 174 | std::ratio_multiply, std::ratio<15,7> >::den == 7 175 | ``` 176 | 177 | ## D.6.5 std::ratio_divide模板别名 178 | 179 | `std::ratio_divide`模板别名提供两个`std::ratio`数在编译时进行相除(使用有理计算)。 180 | 181 | **定义** 182 | 183 | ``` 184 | template 185 | using ratio_multiply = std::ratio; 186 | ``` 187 | 188 | **先决条件**
189 | R1和R2必须使用`std::ratio`进行初始化。 190 | 191 | **效果**
192 | ratio_add被定义为一个别名,如果两数可以计算,且无溢出,该类型可以表示两个`std::ratio`对象R1和R2的和。如果计算出来的结果溢出了,那么程序里面就有问题了。在算术溢出的情况下,`std::ratio_multiply`应该应该与`std::ratio`相同。 193 | 194 | **例子** 195 | 196 | ``` 197 | std::ratio_divide, std::ratio<2,5> >::num == 5 198 | std::ratio_divide, std::ratio<2,5> >::den == 6 199 | 200 | std::ratio_divide, std::ratio<15,7> >::num == 7 201 | std::ratio_divide, std::ratio<15,7> >::den == 45 202 | ``` 203 | 204 | ## D.6.6 std::ratio_equal类型模板 205 | 206 | `std::ratio_equal`类型模板提供在编译时比较两个`std::ratio`数(使用有理计算)。 207 | 208 | **类型定义** 209 | 210 | ``` 211 | template 212 | class ratio_equal: 213 | public std::integral_constant< 214 | bool,(R1::num == R2::num) && (R1::den == R2::den)> 215 | {}; 216 | ``` 217 | 218 | **先决条件**
219 | R1和R2必须使用`std::ratio`进行初始化。 220 | 221 | **例子** 222 | 223 | ``` 224 | std::ratio_equal, std::ratio<2,6> >::value == true 225 | std::ratio_equal, std::ratio<1,6> >::value == false 226 | std::ratio_equal, std::ratio<2,3> >::value == false 227 | std::ratio_equal, std::ratio<1,3> >::value == true 228 | ``` 229 | 230 | ## D.6.7 std::ratio_not_equal类型模板 231 | 232 | `std::ratio_not_equal`类型模板提供在编译时比较两个`std::ratio`数(使用有理计算)。 233 | 234 | **类型定义** 235 | 236 | ``` 237 | template 238 | class ratio_not_equal: 239 | public std::integral_constant::value> 240 | {}; 241 | ``` 242 | 243 | **先决条件**
244 | R1和R2必须使用`std::ratio`进行初始化。 245 | 246 | **例子** 247 | 248 | ``` 249 | std::ratio_not_equal, std::ratio<2,6> >::value == false 250 | std::ratio_not_equal, std::ratio<1,6> >::value == true 251 | std::ratio_not_equal, std::ratio<2,3> >::value == true 252 | std::ratio_not_equal, std::ratio<1,3> >::value == false 253 | ``` 254 | 255 | ## D.6.8 std::ratio_less类型模板 256 | 257 | `std::ratio_less`类型模板提供在编译时比较两个`std::ratio`数(使用有理计算)。 258 | 259 | **类型定义** 260 | 261 | ``` 262 | template 263 | class ratio_less: 264 | public std::integral_constant 265 | {}; 266 | ``` 267 | 268 | **先决条件**
269 | R1和R2必须使用`std::ratio`进行初始化。 270 | 271 | **效果**
272 | std::ratio_less可通过`std::integral_constant`导出,这里value为`(R1::num*R2::den) < (R2::num*R1::den)`。如果有可能,需要实现使用一种机制来避免计算结果已出。当溢出发生,那么程序中就肯定有错误。 273 | 274 | **例子** 275 | 276 | ``` 277 | std::ratio_less, std::ratio<2,6> >::value == false 278 | std::ratio_less, std::ratio<1,3> >::value == true 279 | std::ratio_less< 280 | std::ratio<999999999,1000000000>, 281 | std::ratio<1000000001,1000000000> >::value == true 282 | std::ratio_less< 283 | std::ratio<1000000001,1000000000>, 284 | std::ratio<999999999,1000000000> >::value == false 285 | ``` 286 | 287 | ## D.6.9 std::ratio_greater类型模板 288 | 289 | `std::ratio_greater`类型模板提供在编译时比较两个`std::ratio`数(使用有理计算)。 290 | 291 | **类型定义** 292 | 293 | ``` 294 | template 295 | class ratio_greater: 296 | public std::integral_constant::value> 297 | {}; 298 | ``` 299 | 300 | **先决条件**
301 | R1和R2必须使用`std::ratio`进行初始化。 302 | 303 | ## D.6.10 std::ratio_less_equal类型模板 304 | 305 | `std::ratio_less_equal`类型模板提供在编译时比较两个`std::ratio`数(使用有理计算)。 306 | 307 | **类型定义** 308 | 309 | ``` 310 | template 311 | class ratio_less_equal: 312 | public std::integral_constant::value> 313 | {}; 314 | ``` 315 | 316 | **先决条件**
317 | R1和R2必须使用`std::ratio`进行初始化。 318 | 319 | ## D.6.11 std::ratio_greater_equal类型模板 320 | 321 | `std::ratio_greater_equal`类型模板提供在编译时比较两个`std::ratio`数(使用有理计算)。 322 | 323 | **类型定义** 324 | 325 | ``` 326 | template 327 | class ratio_greater_equal: 328 | public std::integral_constant::value> 329 | {}; 330 | ``` 331 | 332 | **先决条件**
333 | R1和R2必须使用`std::ratio`进行初始化。 -------------------------------------------------------------------------------- /content/appendix_D/D.7-chinese.md: -------------------------------------------------------------------------------- 1 | # D.7 <thread>头文件 2 | 3 | ``头文件提供了管理和辨别线程的工具,并且提供函数,可让当前线程休眠。 4 | 5 | **头文件内容** 6 | 7 | ``` 8 | namespace std 9 | { 10 | class thread; 11 | 12 | namespace this_thread 13 | { 14 | thread::id get_id() noexcept; 15 | 16 | void yield() noexcept; 17 | 18 | template 19 | void sleep_for( 20 | std::chrono::duration sleep_duration); 21 | 22 | template 23 | void sleep_until( 24 | std::chrono::time_point wake_time); 25 | } 26 | } 27 | ``` 28 | 29 | ## D.7.1 std::thread类 30 | 31 | `std::thread`用来管理线程的执行。其提供让新的线程执行或执行,也提供对线程的识别,以及提供其他函数用于管理线程的执行。 32 | 33 | ``` 34 | class thread 35 | { 36 | public: 37 | // Types 38 | class id; 39 | typedef implementation-defined native_handle_type; // optional 40 | 41 | // Construction and Destruction 42 | thread() noexcept; 43 | ~thread(); 44 | 45 | template 46 | explicit thread(Callable&& func,Args&&... args); 47 | 48 | // Copying and Moving 49 | thread(thread const& other) = delete; 50 | thread(thread&& other) noexcept; 51 | 52 | thread& operator=(thread const& other) = delete; 53 | thread& operator=(thread&& other) noexcept; 54 | 55 | void swap(thread& other) noexcept; 56 | 57 | void join(); 58 | void detach(); 59 | bool joinable() const noexcept; 60 | 61 | id get_id() const noexcept; 62 | native_handle_type native_handle(); 63 | static unsigned hardware_concurrency() noexcept; 64 | }; 65 | 66 | void swap(thread& lhs,thread& rhs); 67 | ``` 68 | 69 | ### std::thread::id 类 70 | 71 | 可以通过`std::thread::id`实例对执行线程进行识别。 72 | 73 | **类型定义** 74 | 75 | ``` 76 | class thread::id 77 | { 78 | public: 79 | id() noexcept; 80 | }; 81 | 82 | bool operator==(thread::id x, thread::id y) noexcept; 83 | bool operator!=(thread::id x, thread::id y) noexcept; 84 | bool operator<(thread::id x, thread::id y) noexcept; 85 | bool operator<=(thread::id x, thread::id y) noexcept; 86 | bool operator>(thread::id x, thread::id y) noexcept; 87 | bool operator>=(thread::id x, thread::id y) noexcept; 88 | 89 | template 90 | basic_ostream& 91 | operator<< (basic_ostream&& out, thread::id id); 92 | ``` 93 | 94 | **Notes**
95 | `std::thread::id`的值可以识别不同的执行,每个`std::thread::id`默认构造出来的值都不一样,不同值代表不同的执行线程。 96 | 97 | `std::thread::id`的值是不可预测的,在同一程序中的不同线程的id也不同。 98 | 99 | `std::thread::id`是可以CopyConstructible(拷贝构造)和CopyAssignable(拷贝赋值),所以对于`std::thread::id`的拷贝和赋值是没有限制的。 100 | 101 | #### std::thread::id 默认构造函数 102 | 103 | 构造一个`std::thread::id`对象,其不能表示任何执行线程。 104 | 105 | **声明** 106 | 107 | ``` 108 | id() noexcept; 109 | ``` 110 | 111 | **效果**
112 | 构造一个`std::thread::id`实例,不能表示任何一个线程值。 113 | 114 | **抛出**
115 | 无 116 | 117 | **NOTE** 所有默认构造的`std::thread::id`实例存储的同一个值。 118 | 119 | #### std::thread::id 相等比较操作 120 | 121 | 比较两个`std::thread::id`的值,看是两个执行线程是否相等。 122 | 123 | **声明** 124 | 125 | ``` 126 | bool operator==(std::thread::id lhs,std::thread::id rhs) noexcept; 127 | ``` 128 | 129 | **返回**
130 | 当lhs和rhs表示同一个执行线程或两者不代表没有任何线程,则返回true。当lsh和rhs表示不同执行线程或其中一个代表一个执行线程,另一个不代表任何线程,则返回false。 131 | 132 | **抛出**
133 | 无 134 | 135 | #### std::thread::id 不相等比较操作 136 | 137 | 比较两个`std::thread::id`的值,看是两个执行线程是否相等。 138 | 139 | **声明** 140 | 141 | ``` 142 | bool operator!=(std::thread::id lhs,std::thread::id rhs) noexcept; 143 | ``` 144 | 145 | **返回**
146 | `!(lhs==rhs)` 147 | 148 | **抛出**
149 | 无 150 | 151 | #### std::thread::id 小于比较操作 152 | 153 | 比较两个`std::thread::id`的值,看是两个执行线程哪个先执行。 154 | 155 | **声明** 156 | 157 | ``` 158 | bool operator<(std::thread::id lhs,std::thread::id rhs) noexcept; 159 | ``` 160 | 161 | **返回**
162 | 当lhs比rhs的线程ID靠前,则返回true。当lhs!=rhs,且`lhs 165 | 无 166 | 167 | **NOTE** 当默认构造的`std::thread::id`实例,在不代表任何线程的时候,其值小于任何一个代表执行线程的实例。当两个实例相等,那么两个对象代表两个执行线程。任何一组不同的`std::thread::id`的值,是由同一序列构造,这与程序执行的顺序相同。同一个可执行程序可能有不同的执行顺序。 168 | 169 | #### std::thread::id 小于等于比较操作 170 | 171 | 比较两个`std::thread::id`的值,看是两个执行线程的ID值是否相等,或其中一个先行。 172 | 173 | **声明** 174 | 175 | ``` 176 | bool operator<(std::thread::id lhs,std::thread::id rhs) noexcept; 177 | ``` 178 | 179 | **返回**
180 | `!(rhs 183 | 无 184 | 185 | #### std::thread::id 大于比较操作 186 | 187 | 比较两个`std::thread::id`的值,看是两个执行线程的是后行的。 188 | 189 | **声明** 190 | 191 | ``` 192 | bool operator>(std::thread::id lhs,std::thread::id rhs) noexcept; 193 | ``` 194 | 195 | **返回**
196 | `rhs 199 | 无 200 | 201 | #### std::thread::id 大于等于比较操作 202 | 203 | 比较两个`std::thread::id`的值,看是两个执行线程的ID值是否相等,或其中一个后行。 204 | 205 | **声明** 206 | 207 | ``` 208 | bool operator>=(std::thread::id lhs,std::thread::id rhs) noexcept; 209 | ``` 210 | 211 | **返回**
212 | `!(lhs 215 | 无 216 | 217 | #### std::thread::id 插入流操作 218 | 219 | 将`std::thread::id`的值通过给指定流写入字符串。 220 | 221 | **声明** 222 | 223 | ``` 224 | template 225 | basic_ostream& 226 | operator<< (basic_ostream&& out, thread::id id); 227 | ``` 228 | 229 | **效果**
230 | 将`std::thread::id`的值通过给指定流插入字符串。 231 | 232 | **返回**
233 | 无 234 | 235 | **NOTE** 字符串的格式并未给定。`std::thread::id`实例具有相同的表达式时,是相同的;当实例表达式不同,则代表不同的线程。 236 | 237 | ### std::thread::native_handler 成员函数 238 | 239 | `native_handle_type`是由另一类型定义而来,这个类型会随着指定平台的API而变化。 240 | 241 | **声明** 242 | 243 | ``` 244 | typedef implementation-defined native_handle_type; 245 | ``` 246 | 247 | **NOTE** 这个类型定义是可选的。如果提供,实现将使用原生平台指定的API,并提供合适的类型作为实现。 248 | 249 | ### std::thread 默认构造函数 250 | 251 | 返回一个`native_handle_type`类型的值,这个值可以可以表示*this相关的执行线程。 252 | 253 | **声明** 254 | 255 | ``` 256 | native_handle_type native_handle(); 257 | ``` 258 | 259 | **NOTE** 这个函数是可选的。如果提供,会使用原生平台指定的API,并返回合适的值。 260 | 261 | ### std::thread 构造函数 262 | 263 | 构造一个无相关线程的`std::thread`对象。 264 | 265 | **声明** 266 | 267 | ``` 268 | thread() noexcept; 269 | ``` 270 | 271 | **效果**
272 | 构造一个无相关线程的`std::thread`实例。 273 | 274 | **后置条件**
275 | 对于一个新构造的`std::thread`对象x,x.get_id() == id()。 276 | 277 | **抛出**
278 | 无 279 | 280 | ### std::thread 移动构造函数 281 | 282 | 将已存在`std::thread`对象的所有权,转移到新创建的对象中。 283 | 284 | **声明** 285 | 286 | ``` 287 | thread(thread&& other) noexcept; 288 | ``` 289 | 290 | **效果**
291 | 构造一个`std::thread`实例。与other相关的执行线程的所有权,将转移到新创建的`std::thread`对象上。否则,新创建的`std::thread`对象将无任何相关执行线程。 292 | 293 | **后置条件**
294 | 对于一个新构建的`std::thread`对象x来说,x.get_id()等价于未转移所有权时的other.get_id()。get_id()==id()。 295 | 296 | **抛出**
297 | 无 298 | 299 | **NOTE** `std::thread`对象是不可CopyConstructible(拷贝构造),所以该类没有拷贝构造函数,只有移动构造函数。 300 | 301 | ### std::thread 析构函数 302 | 303 | 销毁`std::thread`对象。 304 | 305 | **声明** 306 | 307 | ``` 308 | ~thread(); 309 | ``` 310 | 311 | **效果**
312 | 销毁`*this`。当`*this`与执行线程相关(this->joinable()将返回true),调用`std::terminate()`来终止程序。 313 | 314 | **抛出**
315 | 无 316 | 317 | ### std::thread 移动赋值操作 318 | 319 | 将一个`std::thread`的所有权,转移到另一个`std::thread`对象上。 320 | 321 | **声明** 322 | 323 | ``` 324 | thread& operator=(thread&& other) noexcept; 325 | ``` 326 | 327 | **效果**
328 | 在调用该函数前,this->joinable返回true,则调用`std::terminate()`来终止程序。当other在执行赋值前,具有相关的执行线程,那么执行线程现在就与`*this`相关联。否则,`*this`无相关执行线程。 329 | 330 | **后置条件**
331 | this->get_id()的值等于调用该函数前的other.get_id()。oter.get_id()==id()。 332 | 333 | **抛出**
334 | 无 335 | 336 | **NOTE** `std::thread`对象是不可CopyAssignable(拷贝赋值),所以该类没有拷贝赋值函数,只有移动赋值函数。 337 | 338 | ### std::thread::swap 成员函数 339 | 340 | 将两个`std::thread`对象的所有权进行交换。 341 | 342 | **声明** 343 | 344 | ``` 345 | void swap(thread& other) noexcept; 346 | ``` 347 | 348 | **效果**
349 | 当other在执行赋值前,具有相关的执行线程,那么执行线程现在就与`*this`相关联。否则,`*this`无相关执行线程。对于`*this`也是一样。 350 | 351 | **后置条件**
352 | this->get_id()的值等于调用该函数前的other.get_id()。other.get_id()的值等于没有调用函数前this->get_id()的值。 353 | 354 | **抛出**
355 | 无 356 | 357 | ### std::thread的非成员函数swap 358 | 359 | 将两个`std::thread`对象的所有权进行交换。 360 | 361 | **声明** 362 | 363 | ``` 364 | void swap(thread& lhs,thread& rhs) noexcept; 365 | ``` 366 | 367 | **效果**
368 | lhs.swap(rhs) 369 | 370 | **抛出**
371 | 无 372 | 373 | ### std::thread::joinable 成员函数 374 | 375 | 查询*this是否具有相关执行线程。 376 | 377 | **声明** 378 | 379 | ``` 380 | bool joinable() const noexcept; 381 | ``` 382 | 383 | **返回**
384 | 如果*this具有相关执行线程,则返回true;否则,返回false。 385 | 386 | **抛出**
387 | 无 388 | 389 | ### std::thread::join 成员函数 390 | 391 | 等待*this相关的执行线程结束。 392 | 393 | **声明** 394 | 395 | ``` 396 | void join(); 397 | ``` 398 | 399 | **先决条件**
400 | this->joinable()返回true。 401 | 402 | **效果**
403 | 阻塞当前线程,直到与*this相关的执行线程执行结束。 404 | 405 | **后置条件**
406 | this->get_id()==id()。与*this先关的执行线程将在该函数调用后结束。 407 | 408 | **同步**
409 | 想要在*this上成功的调用该函数,则需要依赖有joinable()的返回。 410 | 411 | **抛出**
412 | 当效果没有达到或this->joinable()返回false,则抛出`std::system_error`异常。 413 | 414 | ### std::thread::detach 成员函数 415 | 416 | 将*this上的相关线程进行分离。 417 | 418 | **声明** 419 | 420 | ``` 421 | void detach(); 422 | ``` 423 | 424 | **先决条件**
425 | this->joinable()返回true。 426 | 427 | **效果**
428 | 将*this上的相关线程进行分离。 429 | 430 | **后置条件**
431 | this->get_id()==id(), this->joinable()==false 432 | 433 | 与*this相关的执行线程在调用该函数后就会分离,并且不在会与当前`std::thread`对象再相关。 434 | 435 | **抛出**
436 | 当效果没有达到或this->joinable()返回false,则抛出`std::system_error`异常。 437 | 438 | ### std::thread::get_id 成员函数 439 | 440 | 返回`std::thread::id`的值来表示*this上相关执行线程。 441 | 442 | **声明** 443 | 444 | ``` 445 | thread::id get_id() const noexcept; 446 | ``` 447 | 448 | **返回**
449 | 当*this具有相关执行线程,将返回`std::thread::id`作为识别当前函数的依据。否则,返回默认构造的`std::thread::id`。 450 | 451 | **抛出**
452 | 无 453 | 454 | ### std::thread::hardware_concurrency 静态成员函数 455 | 456 | 返回硬件上可以并发线程的数量。 457 | 458 | **声明** 459 | 460 | ``` 461 | unsigned hardware_concurrency() noexcept; 462 | ``` 463 | 464 | **返回**
465 | 硬件上可以并发线程的数量。这个值可能是系统处理器的数量。当信息不用或只有定义,则该函数返回0。 466 | 467 | **抛出**
468 | 无 469 | 470 | ## D.7.2 this_thread命名空间 471 | 472 | 这里介绍一下`std::this_thread`命名空间内提供的函数操作。 473 | 474 | ### this_thread::get_id 非成员函数 475 | 476 | 返回`std::thread::id`用来识别当前执行线程。 477 | 478 | **声明** 479 | 480 | ``` 481 | thread::id get_id() noexcept; 482 | ``` 483 | 484 | **返回**
485 | 可通过`std:thread::id`来识别当前线程。 486 | 487 | **抛出**
488 | 无 489 | 490 | ### this_thread::yield 非成员函数 491 | 492 | 该函数用于通知库,调用线程不需要立即运行。一般使用小循环来避免消耗过多CPU时间。 493 | 494 | **声明** 495 | 496 | ``` 497 | void yield() noexcept; 498 | ``` 499 | 500 | **效果**
501 | 使用标准库的实现来安排线程的一些事情。 502 | 503 | **抛出**
504 | 无 505 | 506 | ### this_thread::sleep_for 非成员函数 507 | 508 | 在指定的指定时长内,暂停执行当前线程。 509 | 510 | **声明** 511 | 512 | ``` 513 | template 514 | void sleep_for(std::chrono::duration const& relative_time); 515 | ``` 516 | 517 | **效果**
518 | 在超出relative_time的时长内,阻塞当前线程。 519 | 520 | **NOTE** 线程可能阻塞的时间要长于指定时长。如果可能,逝去的时间由将会由一个稳定时钟决定。 521 | 522 | **抛出**
523 | 无 524 | 525 | ### this_thread::sleep_until 非成员函数 526 | 527 | 暂停指定当前线程,直到到了指定的时间点。 528 | 529 | **声明** 530 | 531 | ``` 532 | template 533 | void sleep_until( 534 | std::chrono::time_point const& absolute_time); 535 | ``` 536 | 537 | **效果**
538 | 在到达absolute_time的时间点前,阻塞当前线程,这个时间点由指定的Clock决定。 539 | 540 | **NOTE** 这里不保证会阻塞多长时间,只有Clock::now()返回的时间等于或大于absolute_time时,阻塞的线程才能被解除阻塞。 541 | 542 | **抛出**
543 | 无 -------------------------------------------------------------------------------- /content/chapter1/1.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第1章 你好,C++的并发世界! 2 | 3 | **本章主要内容** 4 | 5 | - 何谓并发和多线程
6 | - 应用程序为什么要使用并发和多线程
7 | - C++的并发史
8 | - 一个简单的C++多线程程序
9 | 10 | 令C++用户振奋的时刻到了。距初始的C++标准(1998年)发布13年后,C++标准委员会给语言本身,以及标准库,带来了一次重大的变革。 11 | 12 | 新C++标准(也被称为C++11或C++0x)在2011年发布,带来一系列的变革让C++编程更加简单和高效。 13 | 14 | 其中一个最重要的新特性就是对多线程的支持。 15 | 16 | C++标准第一次承认多线程在语言中的存在,并在标准库中为多线程提供组件。这意味着使用C++编写与平台无关的多线程程序成为可能,也为可移植性提供了强有力的保证。与此同时,程序员们为提高应用的性能,对并发的关注也是与日俱增,特别在多线程编程方面。 17 | 18 | 本书是介绍如何使用C++11多线程来编写并发程序,及相关的语言特性和*库工具*(library facilities)。本书以“解释并发和多线程的含义,为什么要使用并发”作为起始点,在对“什么情况下不使用并发”进行阐述之后,将对C++支持的并发方式进行概述;最后,以一个简单的C++并发实例结束这一章。资深的多线程开发人员可以跳过前面的小节。在后面的几个章节中,会有更多的例子,以便大家对库工具进行更加深入的了解。本书最后,将会给出所有多线程与并发相关的C++标准库工具的全面参考。 19 | 20 | 问题来了,何谓并发?何谓多线程? 21 | 22 | -------------------------------------------------------------------------------- /content/chapter1/1.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 1.1 何谓并发 2 | 3 | 最简单和最基本的并发,是指两个或更多独立的活动同时发生。 4 | 5 | 并发在生活中随处可见,我们可以一边走路一边说话,也可以两只手同时作不同的动作,还有我们每个人都过着相互独立的生活——当我在游泳的时候,你可以看球赛,等等。 6 | 7 | ## 1.1.1 计算机系统中的并发 8 | 9 | 计算机领域的并发指的是在单个系统里同时执行多个独立的任务,而非顺序的进行一些活动。 10 | 11 | 计算机领域里,并发不是一个新事物:很多年前,一台计算机就能通过多任务操作系统的切换功能,同时运行多个应用程序;高端多处理器服务器在很早就已经实现了真正的并行计算。那“老东西”上有哪些“新东西”能让它在计算机领域越来越流行呢?——真正任务并行,而非一种错觉。 12 | 13 | 以前,大多数计算机只有一个处理器,具有单个*处理单元*(processing unit)或*核心*(core),如今还有很多这样的台式机。这种机器只能在某一时刻执行一个任务,不过它可以每秒进行多次任务切换。通过“这个任务做一会,再切换到别的任务,再做一会儿”的方式,让任务看起来是并行执行的。这种方式称为*任务切换*。如今,我们仍然将这样的系统称为*并发*:因为任务切换得太快,以至于无法感觉到任务在何时会被暂时挂起,而切换到另一个任务。任务切换会给用户和应用程序造成一种“并发的假象”。因为这种假象,当应用在任务切换的环境下和真正并发环境下执行相比,行为还是有着微妙的不同。特别是对内存模型不正确的假设(详见第5章),在多线程环境中可能不会出现(详见第10章)。 14 | 15 | 多处理器计算机用于服务器和高性能计算已有多年。基于单芯多核处理器(多核处理器)的台式机,也越来越大众化。无论拥有几个处理器,这些机器都能够真正的并行多个任务。我们称其为*硬件并发*(hardware concurrency)”。 16 | 17 | 图1.1显示了一个计算机处理恰好两个任务时的理想情景,每个任务被分为10个相等大小的块。在一个双核机器(具有两个处理核心)上,每个任务可以在各自的处理核心上执行。在单核机器上做任务切换时,每个任务的块交织进行。但它们中间有一小段分隔(图中所示灰色分隔条的厚度大于双核机器的分隔条);为了实现交织进行,系统每次从一个任务切换到另一个时都需要切换一次*上下文*(context switch),任务切换也有时间开销。进行上下文的切换时,操作系统必须为当前运行的任务保存CPU的状态和指令指针,并计算出要切换到哪个任务,并为即将切换到的任务重新加载处理器状态。然后,CPU可能要将新任务的指令和数据的内存载入到缓存中,这会阻止CPU执行任何指令,从而造成的更多的延迟。 18 | 19 | ![](../../images/chapter1/1-1.png) 20 | 21 | 图 1.1 并发的两种方式:双核机器的真正并行 Vs. 单核机器的任务切换 22 | 23 | 有些处理器可以在一个核心上执行多个线程,但硬件并发在多处理器或多核系统上效果更加显著。*硬件线程*最重要的因素是数量,也就是硬件上可以并发运行多少独立的任务。即便是具有真正硬件并发的系统,也很容易拥有比硬件“可并行最大任务数”还要多的任务需要执行,所以任务切换在这些情况下仍然适用。例如,在一个典型的台式计算机上可能会有成百上千个的任务在运行,即便是在计算机处于空闲时,还是会有后台任务在运行。正是任务切换使得这些后台任务可以运行,并使得你可以同时运行文字处理器、编译器、编辑器和web浏览器(或其他应用的组合)。图1.2显示了四个任务在双核处理器上的任务切换,仍然是将任务整齐地划分为同等大小块的理想情况。实际上,许多因素会使得分割不均和调度不规则。部分因素将在第8章中讨论,那时我们再来看一看影响并行代码性能的因素。 24 | 25 | 无论应用程序在单核处理器,还是多核处理器上运行;也不论是任务切换还是真正的硬件并发,这里提到的技术、功能和类(本书所涉及的)都能使用得到。如何使用并发,将很大程度上取决于可用的硬件并发。我们将在第8章中再次讨论这个问题,并具体研究C++代码并行设计的问题。 26 | 27 | ![](../../images/chapter1/1-2.png) 28 | 29 | 图 1.2 四个任务在两个核心之间的切换 30 | 31 | ## 1.1.2 并发的途径 32 | 33 | 试想当两个程序员在两个独立的办公室一起做一个软件项目,他们可以安静地工作、不互相干扰,并且他们人手一套参考手册。但是,他们沟通起来就有些困难,比起可以直接互相交谈,他们必须使用电话、电子邮件或到对方的办公室进行直接交流。并且,管理两个办公室需要有一定的经费支出,还需要购买多份参考手册。 34 | 35 | 假设,让开发人员同在一间办公室办公,他们可以自由的对某个应用程序设计进行讨论,也可以在纸或白板上轻易的绘制图表,对设计观点进行辅助性阐释。现在,你只需要管理一个办公室,只要有一套参考资料就够了。遗憾的是,开发人员可能难以集中注意力,并且还可能存在资源共享的问题(比如,“参考手册哪去了?”) 36 | 37 | 以上两种方法,描绘了并发的两种基本途径。每个开发人员代表一个线程,每个办公室代表一个进程。第一种途径是每个进程只要一个线程,这就类似让每个开发人员拥有自己的办公室,而第二种途径是每个进程有多个线程,如同一个办公室里有两个开发人员。让我们在一个应用程序中简单的分析一下这两种途径。 38 | 39 | ##### 多进程并发 40 | 41 | 使用并发的第一种方法,是将应用程序分为多个独立的进程,它们在同一时刻运行,就像同时进行网页浏览和文字处理一样。如图1.3所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号、套接字、文件、管道等等)。不过,这种进程之间的通信通常不是设置复杂,就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以避免一个进程去修改另一个进程的数据。还有一个缺点是,运行多个进程所需的固定开销:需要时间启动进程,操作系统需要内部资源来管理进程,等等。 42 | 43 | 当然,以上的机制也不是一无是处:操作系统在进程间提供附加的保护操作和更高级别的通信机制,意味着可以更容易编写安全的并发代码。实际上,在类似于Erlang的编程环境中,将进程作为并发的基本构造块。 44 | 45 | 使用多进程实现并发还有一个额外的优势———可以使用远程连接(可能需要联网)的方式,在不同的机器上运行独立的进程。虽然,这增加了通信成本,但在设计精良的系统上,这可能是一个提高并行可用行和性能的低成本方式。 46 | 47 | ![](../../images/chapter1/1-3.png) 48 | 49 | 图 1.3 一对并发运行的进程之间的通信 50 | 51 | ##### 多线程并发 52 | 53 | 并发的另一个途径,在单个进程中运行多个线程。线程很像轻量级的进程:每个线程相互独立运行,且线程可以在不同的指令序列中运行。但是,进程中的所有线程都共享地址空间,并且所有线程访问到大部分数据———全局变量仍然是全局的,指针、对象的引用或数据可以在线程之间传递。虽然,进程之间通常共享内存,但是这种共享通常是难以建立和管理的。因为,同一数据的内存地址在不同的进程中是不相同。图1.4展示了一个进程中的两个线程通过共享内存进行通信。 54 | 55 | ![](../../images/chapter1/1-4.png) 56 | 57 | 图 1.4 同一进程中的一对并发运行的线程之间的通信 58 | 59 | 地址空间共享,以及缺少线程间数据的保护,使得操作系统的记录工作量减小,所以使用多线程相关的开销远远小于使用多个进程。不过,共享内存的灵活性是有代价的:如果数据要被多个线程访问,那么程序员必须确保每个线程所访问到的数据是一致的(在本书第3、4、5和8章中会涉及,线程间数据共享可能会遇到的问题,以及如何使用工具来避免这些问题)。问题并非无解,只要在编写代码时适当地注意即可,这同样也意味着需要对线程通信做大量的工作。 60 | 61 | 多个单线程/进程间的通信(包含启动)要比单一进程中的多线程间的通信(包括启动)的开销大,若不考虑共享内存可能会带来的问题,多线程将会成为主流语言(包括`C++`)更青睐的并发途径。此外,`C++`标准并未对进程间通信提供任何原生支持,所以使用多进程的方式实现,这会依赖与平台相关的API。因此,本书只关注使用多线程的并发,并且在此之后所提到“并发”,均假设为多线程来实现。 62 | 63 | 了解并发后,让来看看为什么要使用并发。 -------------------------------------------------------------------------------- /content/chapter1/1.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 1.2 为什么使用并发? 2 | 3 | 主要原因有两个:关注点分离(SOC)和性能。事实上,它们应该是使用并发的唯一原因;如果你观察得足够仔细,所有因素都可以归结到其中的一个原因(或者可能是两个都有。当然,除了像“就因为我愿意”这样的原因之外)。 4 | 5 | ## 1.2.1 为了分离关注点 6 | 7 | 编写软件时,分离关注点是个好主意;通过将相关的代码与无关的代码分离,可以使程序更容易理解和测试,从而减少出错的可能性。即使一些功能区域中的操作需要在同一时刻发生的情况下,依旧可以使用并发分离不同的功能区域;若不显式地使用并发,就得编写一个任务切换框架,或者在操作中主动地调用一段不相关的代码。 8 | 9 | 考虑一个有用户界面的处理密集型应用——DVD播放程序。这样的应用程序,应具备这两种功能:一,要从光盘中读出数据,对图像和声音进行解码,之后把解码出的信号输出至视频和音频硬件,从而实现DVD的无误播放;二,还需要接受来自用户的输入,当用户单击“暂停”、“返回菜单”或“退出”按键的时候执行对应的操作。当应用是单个线程时,应用需要在回放期间定期检查用户的输入,这就需要把“DVD播放”代码和“用户界面”代码放在一起,以便调用。如果使用多线程方式来分隔这些关注点,“用户界面”代码和“DVD播放”代码就不再需要放在一起:一个线程可以处理“用户界面”事件,另一个进行“DVD播放”。它们之间会有交互(用户点击“暂停”),不过任务间需要人为的进行关联。 10 | 11 | 这会给响应性带来一些错觉,因为用户界面线程通常可以立即响应用户的请求,在当请求传达给忙碌线程,这时的相应可以是简单地显示代表忙碌的光标或“请等待”字样的消息。类似地,独立的线程通常用来执行那些必须在后台持续运行的任务,例如,桌面搜索程序中监视文件系统变化的任务。因为它们之间的交互清晰可辨,所以这种方式会使每个线程的逻辑变的更加简单。 12 | 13 | 在这种情况下,线程的数量不再依赖CPU中的可用内核的数量,因为对线程的划分是基于概念上的设计,而不是一种增加吞吐量的尝试。 14 | 15 | ## 1.2.2 为了性能 16 | 17 | 多处理器系统已经存在了几十年,但直到最近,它们也只在超级计算机、大型机和大型服务器系统中才能看到。然而,芯片制造商越来越倾向于多核芯片的设计,即在单个芯片上集成2、4、16或更多的处理器,从而获取更好的性能。因此,多核台式计算机、多核嵌入式设备,现在越来越普遍。它们计算能力的提高不是源自使单一任务运行的更快,而是并行运行多个任务。在过去,程序员曾坐看他们的程序随着处理器的更新换代而变得更快,无需他们这边做任何事。但是现在,就像Herb Sutter所说的,“没有免费的午餐了。”[1] *如果想要利用日益增长的计算能力,那就必须设计多任务并发式软件*。程序员必须留意这个,尤其是那些迄今都忽略并发的人们,现在很有必要将其加入工具箱中了。 18 | 19 | 两种方式利用并发提高性能:第一,将一个单个任务分成几部分,且各自并行运行,从而降低总运行时间。这就是任务并行(*task parallelism*)。虽然这听起来很直观,但它是一个相当复杂的过程,因为在各个部分之间可能存在着依赖。区别可能是在过程方面——一个线程执行算法的一部分,而另一个线程执行算法的另一个部分——或是在数据方面——每个线程在不同的数据部分上执行相同的操作(第二种方式)。后一种方法被称为数据并行(*data parallelism*)。 20 | 21 | 第一种并行方式影响的算法常被称为易并行(*embarrassingly parallel*)算法。尽管易并行算法的代码会让你感觉到头痛,但这对于你来说是一件好事:我曾遇到过自然并行(*naturally parallel*)和便利并发(*conveniently concurrent*)的算法。易并行算法具有良好的可扩展特性——当可用硬件线程的数量增加时,算法的并行性也会随之增加。这种算法能很好的体现*人多力量大*。如果算法中有不易并行的部分,你可以把算法划分成固定(不可扩展)数量的并行任务。第8章将会再来讨论,在线程之间划分任务的技巧。 22 | 23 | 第二种方法是使用可并行的方式,来解决更大的问题;与其同时处理一个文件,不如酌情处理2个、10个或20个。虽然,这是数据并行的一种应用(通过对多组数据同时执行相同的操作),但着重点不同。处理一个数据块仍然需要同样的时间,但在相同的时间内处理了更多的数据。当然,这种方法也有限制,并非在所有情况下都是有益的。不过,这种方法所带来的吞吐量提升,可以让某些新功能成为可能,例如,可以并行处理图片的各部分,就能提高视频的分辨率。 24 | 25 | ## 1.2.3 什么时候不使用并发 26 | 27 | 知道何时**不使用**并发与知道何时**使用**它一样重要。基本上,不使用并发的唯一原因就是,收益比不上成本。使用并发的代码在很多情况下难以理解,因此编写和维护的多线程代码就会产生直接的脑力成本,同时额外的复杂性也可能引起更多的错误。除非潜在的性能增益足够大或关注点分离地足够清晰,能抵消所需的额外的开发时间以及与维护多线程代码相关的额外成本(代码正确的前提下);否则,别用并发。 28 | 29 | 同样地,性能增益可能会小于预期;因为操作系统需要分配内核相关资源和堆栈空间,所以在启动线程时存在固有的开销,然后才能把新线程加入调度器中,所有这一切都需要时间。如果在线程上的任务完成得很快,那么任务实际执行的时间要比启动线程的时间小很多,这就会导致应用程序的整体性能还不如直接使用“产生线程”的方式。 30 | 31 | 此外,线程是有限的资源。如果让太多的线程同时运行,则会消耗很多操作系统资源,从而使得操作系统整体上运行得更加缓慢。不仅如此,因为每个线程都需要一个独立的堆栈空间,所以运行太多的线程也会耗尽进程的可用内存或地址空间。对于一个可用地址空间为4GB(32bit)的平坦架构的进程来说,这的确是个问题:如果每个线程都有一个1MB的堆栈(很多系统都会这样分配),那么4096个线程将会用尽所有地址空间,不会给代码、静态数据或者堆数据留有任何空间。即便64位(或者更大)的系统不存在这种直接的地址空间限制,但其他资源有限:如果你运行了太多的线程,最终也是出会问题的。尽管线程池(参见第9章)可以用来限制线程的数量,但这也并不是什么灵丹妙药,它也有自己的问题。 32 | 33 | 当客户端/服务器(C/S)应用在服务器端为每一个链接启动一个独立的线程,对于少量的链接是可以正常工作的,但当同样的技术用于需要处理大量链接的高需求服务器时,也会因为线程太多而耗尽系统资源。在这种场景下,使用线程池可以对性能产生优化(参见第9章)。 34 | 35 | 最后,运行越多的线程,操作系统就需要做越多的上下文切换,每一次切换都需要耗费本可以花在有价值工作上的时间。所以在某些时候,增加一个额外的线程实际上会降低,而非提高应用程序的整体性能。为此,如果你试图得到系统的最佳性能,可以考虑使用硬件并发(或不用),并调整运行线程的数量。 36 | 37 | 为性能而使用并发就像所有其他优化策略一样:它拥有大幅度提高应用性能的潜力,但它也可能使代码复杂化,使其更难理解,并更容易出错。因此,只有应用中具有显著增益潜力的性能关键部分,才值得并发化。当然,如果性能收益的潜力仅次于设计清晰或关注点分离,可能也值得使用多线程设计。 38 | 39 | 假设你已经决定确实要在应用中使用并发,无论是为了性能、关注点分离,亦或是因为*多线程星期一*(multithreading Monday)(译者:可能是学习多线程的意思)。 40 | 41 | 问题又来了,对于C++程序员来说,多线程意味着什么? 42 | 43 | ---------- 44 | 45 | [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. -------------------------------------------------------------------------------- /content/chapter1/1.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 1.3 C++中的并发和多线程 2 | 3 | 通过多线程为C++并发提供标准化支持是件新鲜事。只有在C++11标准下,才能编写不依赖平台扩展的多线程代码。了解C++线程库中的众多规则前,先来了解一下其发展的历史。 4 | 5 | ## 1.3.1 C++多线程历史 6 | 7 | C++98(1998)标准不承认线程的存在,并且各种语言要素的操作效果都以顺序抽象机的形式编写。不仅如此,内存模型也没有正式定义,所以在C++98标准下,没办法在缺少编译器相关扩展的情况下编写多线程应用程序。 8 | 9 | 当然,编译器供应商可以自由地向语言添加扩展,添加C语言中流行的多线程API———POSIX标准中的C标准和Microsoft Windows API中的那些———这就使得很多C++编译器供应商通过各种平台相关扩展来支持多线程。这种编译器支持一般受限于只能使用平台相关的C语言API,并且该C++运行库(例如,异常处理机制的代码)能在多线程情况下正常工作。因为编译器和处理器的实际表现很不错了,所以在少数编译器供应商提供正式的多线程感知内存模型之前,程序员们已经编写了大量的C++多线程程序了。 10 | 11 | 由于不满足于使用平台相关的C语言API来处理多线程,C++程序员们希望使用的类库能提供面向对象的多线程工具。像MFC这样的应用框架,如同Boost和ACE这样的已积累了多组类的通用C++类库,这些类封装了底层的平台相关API,并提供用来简化任务的高级多线程工具。各种类和库在细节方面差异很大,但在启动新线程的方面,总体构造却大同小异。一个为许多C++类和库共有的设计,同时也是为程序员提供很大便利的设计,也就是使用带锁的*获取资源即初始化*(RAII, Resource Acquisition Is Initialization)的习惯,来确保当退出相关作用域时互斥元解锁。 12 | 13 | 编写多线程代码需要坚实的编程基础,当前的很多C++编译器为多线程编程者提供了对应(平台相关)的API;当然,还有一些与平台无关的C++类库(例如:Boost和ACE)。正因为如此,程序员们可以通过这些API来实现多线程应用。不过,由于缺乏统一标准的支持,缺少统一的线程内存模型,进而导致一些问题,这些问题在跨硬件或跨平台相关的多线程应用上表现得尤为明显。 14 | 15 | ## 1.3.2 新标准支持并发 16 | 17 | 所有的这些随着C++11标准的发布而改变了,新标准中不仅有了一个全新的线程感知内存模型,C++标准库也扩展了:包含了用于管理线程(参见第2章)、保护共享数据(参见第3章)、线程间同步操作(参见第4章),以及低级原子操作(参见第5章)的各种类。 18 | 19 | 新C++线程库很大程度上,是基于上文提到的C++类库的经验积累。特别是,Boost线程库作为新类库的主要模型,很多类与Boost库中的相关类有着相同名称和结构。随着C++标准的进步,Boost线程库也配合着C++标准在许多方面做出改变,因此之前使用Boost的用户将会发现自己非常熟悉C++11的线程库。 20 | 21 | 如本章起始提到的那样,支持并发仅仅是C++标准的变化之一,此外还有很多对于编程语言自身的改善,就是为了让程序员们的工作变得更加轻松。这些内容在本书的论述范围之外,但是其中的一些变化对于线程库本身及其使用方式产生了很大的影响。附录A会对这些特性做一些介绍。 22 | 23 | 新的C++标准直接支持原子操作,允许程序员通过定义语义的方式编写高效的代码,从而无需了解与平台相关的汇编指令。这对于试图编写高效、可移植代码的程序员们来说是一个好消息;编译器不仅可以搞定具体平台,还可以编写优化器来解释操作语义,从而让程序整体得到更好的优化。 24 | 25 | ## 1.3.3 C++线程库的效率 26 | 27 | 通常情况下,这是高性能计算开发者对C++的担忧之一。为了效率,C++类整合了一些底层工具。这样就需要了解相关使用高级工具和使用低级工具的开销差,这个开销差就是*抽象代价*(abstraction penalty)。 28 | 29 | C++标准委员会在设计标准库时,特别是设计标准线程库的时候,就已经注意到了这点;目的就是在实现相同功能的前提下,直接使用底层API并不会带来过多的性能收益。因此,该类库在大部分主流平台上都能实现高效(带有非常低的抽象代价)。 30 | 31 | C++标准委员会为了达到终极性能,需要确保C++能给那些要与硬件打交道的程序员,提供足够多的的底层工具。为了这个目的,伴随着新的内存模型,出现了一个综合的原子操作库,可用于直接控制单个位、字节、内部线程间同步,以及所有变化的可见性。原子类型和相应的操作现在可以在很多地方使用,而这些地方以前可能使用的是平台相关的汇编代码。使用了新标准的代码会具有更好的可移植性,而且更容易维护。 32 | 33 | C++标准库也提供了更高级别的抽象和工具,使得编写多线程代码更加简单,并且不易出错。有时运用这些工具确实会带来性能开销,因为有额外的代码必须执行。但是,这种性能成本并不一定意味着更高的抽象代价;总体来看,这种性能开销并不比手工编写等效函数高,而且编译器可能会很好地内联大部分额外代码。 34 | 35 | 某些情况下,高级工具会提供一些额外的功能。大部分情况下这都不是问题,因为你没有为你不使用的那部分买单。在罕见的情况下,这些未使用的功能会影响其他代码的性能。如果你很看重程序的性能,并且高级工具带来的开销过高,你最好是通过较低级别的工具来实现你需要的功能。绝大多数情况下,额外增加的复杂性和出错几率都远大于性能的小幅提升带来的收益。即便是有证据确实表明瓶颈出现在C++标准库的工具中,也可能会归咎于低劣的应用设计,而非低劣的类库实现。例如,如果过多的线程竞争一个互斥单元,将会很明显的影响性能。与其在互斥操作上耗费时间,不如重新设计应用,减少互斥元上的竞争来得划算。如何减少应用中的竞争,会在第8章中再次提及。 36 | 37 | 在C++标准库没有提供所需的性能或行为时,就需要使用与平台相关的工具。 38 | 39 | ## 1.3.4 平台相关的工具 40 | 41 | 虽然C++线程库为多线程和并发处理提供了较全面的工具,但在某些平台上提供额外的工具。为了方便地访问那些工具的同时,又使用标准C++线程库,在C++线程库中提供一个`native_handle()`成员函数,允许通过使用平台相关API直接操作底层实现。就其本质而言,任何使用`native_handle()`执行的操作都是完全依赖于平台的,这超出了本书(同时也是标准C++库本身)的范围。 42 | 43 | 所以,使用平台相关的工具之前,要明白标准库能够做什么,那么下面通过一个栗子来展示下吧。 -------------------------------------------------------------------------------- /content/chapter1/1.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 1.4 开始入门 2 | 3 | ok!现在你有一个能与C++11标准兼容的编译器。接下来呢?一个C++多线程程序是什么样子呢?其实,它看上去和其他C++程序差不多,通常是变量、类以及函数的组合。唯一的区别在于某些函数可以并发运行,所以需要确保共享数据在并发访问时是安全的,详见第3章。当然,为了并发地运行函数,必须使用特定的函数以及对象来管理各个线程。 4 | 5 | ## 1.4.1 你好,并发世界 6 | 7 | 从一个经典的例子开始:一个打印“Hello World.”的程序。一个非常简单的在单线程中运行的Hello World程序如下所示,当我们谈到多线程时,它可以作为一个基准。 8 | 9 | ```c++ 10 | #include 11 | int main() 12 | { 13 | std::cout << "Hello World\n"; 14 | } 15 | ``` 16 | 17 | 这个程序所做的就是将“Hello World”写进标准输出流。让我们将它与下面清单所示的简单的“Hello, Concurrent World”程序做个比较,它启动了一个独立的线程来显示这个信息。 18 | 19 | 清单 1.1 一个简单的Hello, Concurrent World程序: 20 | 21 | ``` 22 | #include 23 | #include //① 24 | void hello() //② 25 | { 26 | std::cout << "Hello Concurrent World\n"; 27 | } 28 | int main() 29 | { 30 | std::thread t(hello); //③ 31 | t.join(); //④ 32 | } 33 | ``` 34 | 35 | 第一个区别是增加了`#include `①,标准C++库中对多线程支持的声明在新的头文件中:管理线程的函数和类在``中声明,而保护共享数据的函数和类在其他头文件中声明。 36 | 37 | 其次,打印信息的代码被移动到了一个独立的函数中②。因为每个线程都必须具有一个*初始函数*(initial function),新线程的执行从这里开始。对于应用程序来说,初始线程是main(),但是对于其他线程,可以在`std::thread`对象的构造函数中指定——本例中,被命名为t③的`std::thread`对象拥有新函数hello()作为其初始函数。 38 | 39 | 下一个区别:与直接写入标准输出或是从main()调用hello()不同,该程序启动了一个全新的线程来实现,将线程数量一分为二——初始线程始于main(),而新线程始于hello()。 40 | 41 | 新的线程启动之后③,初始线程继续执行。如果它不等待新线程结束,它就将自顾自地继续运行到main()的结束,从而结束程序——有可能发生在新线程运行之前。这就是为什么在④这里调用`join()`的原因——详见第2章,这会导致调用线程(在main()中)等待与`std::thread`对象相关联的线程,即这个例子中的t。 42 | 43 | 这看起来仅仅为了将一条信息写入标准输出而做了大量的工作,确实如此——正如上文1.2.3节所描述的,一般来说并不值得为了如此简单的任务而使用多线程,尤其是在这期间初始线程并没做什么。本书后面的内容中,将通过实例来展示在哪些情景下使用多线程可以获得收益。 -------------------------------------------------------------------------------- /content/chapter1/1.5-chinese.md: -------------------------------------------------------------------------------- 1 | # 1.5 本章总结 2 | 3 | 本章中,提及了并发与多线程的含义,以及在你的应用程序中为什么你会选择使用(或不使用)它。还提及了多线程在C++中的发展历程,从1998标准中完全缺乏支持,经历了各种平台相关的扩展,再到新的C++11标准中具有合适的多线程支持。芯片制造商选择了以多核心的形式,使得更多任务可以同时执行的方式来增加处理能力,而不是增加单个核心的执行速度。在这个趋势下,C++多线程来的正是时候,它使得程序员们可以利用新的CPU,带来的更加强大的硬件并发。 4 | 5 | 使用1.4节中例子,展示C++标准库中的类和函数有多么的简单。C++中使用多线程并不复杂,复杂的是如何设计代码以实现其预期的行为。 6 | 7 | 尝试了1.4节的示例后,是时候看看更多实质性的内容了。 8 | 9 | 第2章中,我们将了解一下用于管理线程的类和函数。 -------------------------------------------------------------------------------- /content/chapter10/10.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第10章 多线程程序的测试和调试 2 | 3 | **本章主要内容** 4 | 5 | - 并发相关的错误
6 | - 定位错误和代码审查
7 | - 设计多线程测试用例
8 | - 多线程代码的性能
9 | 10 | 目前为止,我们了解如何写并发代码——可以使用哪些工具,这些工具应该如何使用。不过,在软件开发中重要的一部分我们还没有提及:测试与调试。如果你希望阅读完本章后就能很轻松的去调试并发代码,本章无法满足你的预期。 11 | 12 | 测试和调试并发代码比较麻烦。除了对一些重要问题的思考,我也会展示一些技巧让测试和调试变得简单一些。 13 | 14 | 测试和调试就像一个硬币的两面——测试是为了找到代码中可能存在的错误,需要调试来修复错误。如果在开发阶段发现了某个错误,而非发布后发现,这将会将使错误的破坏力降低好几个数量级。 15 | 16 | 了解测试和调试前,需要了解并发代码可能会出现的问题。 -------------------------------------------------------------------------------- /content/chapter10/10.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 10.1 与并发相关的错误类型 2 | 3 | 你可以在并发代码中发现各式各样的错误,这些错误不会集中于某个方面。不过,有一些错误与使用并发直接相关,本章重点关注这些错误。通常,并发相关的错误通常有两大类: 4 | 5 | - 不必要阻塞 6 | 7 | - 条件竞争 8 | 9 | 这两大类的颗粒度很大,让我们将其分成颗粒度较小的问题。 10 | 11 | ##10.1.1 不必要阻塞 12 | 13 | “不必要阻塞”是什么意思?一个线程被阻塞的时候,不能处理任何任务,因为它在等待其他“条件”的达成。通常这些“条件”就是一个互斥量、一个条件变量或一个future,也可能是一个I/O操作。这是多线程代码的先天特性,不过这也不是在任何时候都可取的——衍生成“不必要阻塞”。你会问:为什么不需要阻塞?通常,是因为其他线程在等待该阻塞线程上的某些操作完成,如果该线程阻塞了,那那些线程必然会被阻塞。 14 | 15 | 这个主题可以分成以下几个问题: 16 | 17 | - 死锁——如你在第3章所见,在死锁的情况下,两个线程会互相等待。当线程产生死锁,应该完成的任务就会持续搁置。举个例子来说,一些线程是负责对用户界面操作的线程,在死锁的情况下,用户界面就会无响应。在另一些例子中,界面接口会保持响应,不过有些任务就无法完成,比如:查询无结果返回,或文档未打印。 18 | 19 | - 活锁——与死锁的情况类似。不同的地方在于线程不是阻塞等待,而是在循环中持续检查,例如:自旋锁。一些比较严重的情况下,其表现和死锁一样(应用不会做任何处理,停止响应),CPU的使用率还居高不下;因为线程还在循环中被检查,而不是阻塞等待。在一些不太严重的情况下,因为使用随机调度,活锁的问题还是可以解决的。 20 | 21 | - I/O阻塞或外部输入——当线程被外部输入所阻塞,线程也就不能做其他事情了(即使,等待输入的情况永远不会发生)。因此,被外部输入所阻塞,就会让人不太高兴,因为可能有其他线程正在等待这个线程完成某些任务。 22 | 23 | 简单的介绍了一下“不必要阻塞”的组成。 24 | 25 | 那么,条件竞争呢? 26 | 27 | ## 10.1.2 条件竞争 28 | 29 | 条件竞争在多线程代码中很常见——很多条件竞争表现为死锁与活锁。而且,并非所有条件竞争都是恶性的——对独立线程相关操作的调度,决定了条件竞争发生的时间。很多条件竞争是良性的,比如:哪一个线程去处理任务队列中的下一个任务。不过,很多并发错误的引起也是因为条件竞争。 30 | 31 | 特别是,条件竞争经常会产生以下几种类型的错误: 32 | 33 | - 数据竞争——因为未同步访问一块共享内存,将会导致代码产生未定义行为。在第5章已经介绍了数据竞争,也了解了`C++`的内存模型。数据竞争通常发生在错误的使用原子操作,做同步线程的时候,或没使用互斥量所保护的共享数据的时候。 34 | 35 | - 破坏不变量——主要表现为悬空指针(因为其他线程已经将要访问的数据删除了),随机存储错误(因为局部更新,导致线程读取了不一样的数据),以及双重释放(比如:当两个线程对同一个队列同时执行pop操作,想要删除同一个关联数据),等等。不变量被破坏可以看作为“基于数据”的问题。当独立线程需要以一定顺序执行某些操作时,错误的同步会导致条件竞争,比如:顺序被破坏。 36 | 37 | - 生命周期问题——虽然这类问题也能归结为破坏了不变量,不过这里将其作为一个单独的类别给出。这里的问题是,线程会访问不存在的数据,这可能是因为数据被删除或销毁了,或者转移到其他对象中去了。生命周期问题,通常是在一个线程引用了局部变量,在线程还没有完成前,局部变量的“死期”就已经到了,不过这个问题并不止存在这种情况下。当你手动调用join()等待线程完成工作,你需要保证异常抛出的时候,join()还会等待其他未完成工作的线程。这是线程中基本异常安全的应用。 38 | 39 | 恶性条件竞争就如同一个杀手。死锁和活锁会表现为:应用挂起和反应迟钝,或超长时间完成任务。当一个线程产生死锁或活锁,可以用调试器附着到该线程上进行调试。条件竞争,破坏不变量,以及生命周期问题,其表现都是代码可见的(比如,随机崩溃或错误输出)——可能重写了系统部分的内存使用方式(不会改太多)。其中,可能是因为执行时间,导致问题无法定位到具体的位置。这是共享内存系统的诅咒——需要通过线程尝试限制可访问的数据,并且还要正确的使用同步,应用中的任何线程都可以复写(可被其他线程访问的)数据。 40 | 41 | 现在已经了解了这两大类中都有哪些具体问题了。 42 | 43 | 下面就让我们来了解,如何在你的代码中定位和修复这些问题。 -------------------------------------------------------------------------------- /content/chapter10/10.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 10.2 定位并发错误的技术 2 | 3 | 之前的章节,我们了解了与并发相关的错误类型,以及如何在代码中体现出来的。这些信息可以帮助我们来判断,的代码中是否存在有隐藏的错误。 4 | 5 | 最简单直接的就是直接看代码。虽然看起来比较明显,但是要彻底的修复问题,却是很难的。读刚写完的代码,要比读已经存在的代码容易的多。同理,当在审查别人写好的代码时,给出一个通读结果是很容易的,比如:与你自己的代码标准作对比,以及高亮标出显而易见的问题。为什么要花时间来仔细梳理代码?想想之前提到的并发相关的问题——也要考虑非并发问题。(也可以在很久以后做这件事。不过,最后bug依旧存在)我们可以在审阅代码的时候,考虑一些具体的事情,并且发现问题。 6 | 7 | 即使已经很对代码进行了很详细的审阅,依旧会错过一些bug,这就需要确定一下代码是否做了对应的工作。因此,在测试多线程代码时,会介绍一些代码审阅的技巧。 8 | 9 | ## 10.2.1 代码审阅——发现潜在的错误 10 | 11 | 在审阅多线程代码时,重点要检查与并发相关的错误。如果可能,可以让同事/同伴来审阅。因为不是他们写的代码,他们将会考虑这段代码是怎么工作的,就可能会覆盖到一些你没有想到的情况,从而找出一些潜在的错误。审阅人员需要有时间去做审阅——并非在休闲时间简单的扫一眼。大多数并发问题需要的不仅仅是一次快速浏览——通常需要在找到问题上花费很多时间。 12 | 13 | 如果你让你的同时来审阅代码,他/她肯定对你的代码不是很熟悉。因此,他/她会从不同的角度来看你的代码,然后指出你没有注意的事情。如果你的同事都没有空,你可以叫你的朋友,或传到网络上,让网友审阅(注意,别传一些机密代码上去)。实在没有人审阅你的代码,不要着急——你还可以做很多事情。对于初学者,可以将代码放置一段时间——先去做应用的另外的部分,或是阅读一本书籍,亦或出去溜达溜达。在休息之后,当再集中注意力做某些事情(潜意识会考虑很多问题)。同样,当你做完其他事情,回头再看这段代码,就会有些陌生——你可能会从另一个角度来看你自己以前写的代码。 14 | 15 | 另一种方式就是自己审阅。可以向别人详细的介绍你所写的功能,可能并不是一个真正的人——可能要对一只玩具熊或一只橡皮鸡来进行解释,并且我个人觉得写一些比较详细的注释是非常有益的。在解释过程中,会考虑每一行过后,会发生什么事情,有哪些数据被访问了,等等。问自己关于代码的问题,并且向自己解释这些问题。我觉得这是种非常有效的技巧——通过自问自答,对每个问题认真考虑,这些问题往往都会揭示一些问题,也会有益于任何形式的代码审阅。 16 | 17 | **审阅多线程代码需要考虑的问题** 18 | 19 | 审阅代码的时候考虑和代码相关的问题,以及有利于找出代码中的问题。问题审阅者需要在代码中找到相应的回答或错误。我认为下面这些问题必须要问(当然,不是一个综合性的列表),你也可以找一些其他问题来帮助你找到代码的问题。 20 | 21 | 这里,列一下我的清单: 22 | 23 | - 并发访问时,那些数据需要保护? 24 | 25 | - 如何确定访问数据受到了保护? 26 | 27 | - 是否会有多个线程同时访问这段代码? 28 | 29 | - 这个线程获取了哪个互斥量? 30 | 31 | - 其他线程可能获取哪些互斥量? 32 | 33 | - 两个线程间的操作是否有依赖关系?如何满足这种关系? 34 | 35 | - 这个线程加载的数据还是合法数据吗?数据是否被其他线程修改过? 36 | 37 | - 当假设其他线程可以对数据进行修改,这将意味着什么?并且,怎么确保这样的事情不会发生? 38 | 39 | 最后一个问题,我最喜欢,因为它让我着实的去考虑线程之间的关系。通过假设一个bug和一行代码相关联,你就可以扮演侦探来追踪bug出现的原因。为了让你自己确定代码里面没有bug,需要考虑代码运行的各种情况。在数据被多个互斥量所保护的时候,这种方式尤其有用,比如:使用线程安全队列(第6章),可以对队头和队尾使用独立的互斥量:就是为了确保在持有一个互斥量的时候,访问是安全的,这里必须确保持有其他互斥量的线程不能同时访问同一元素。还需要特别关注的是,对公共数据的显式处理,使用一个指针或引用的方式让其他代码来获取数据。 40 | 41 | 倒数第二个问题也很重要,因为这是很容易产生错误的地方:先释放再获取一个互斥量的前提是,其他线程可能会修改共享数据。虽然很明显,但当互斥锁不是立即可见——可能因为是内部对象——就会不知不觉的掉入陷阱中。在第6章,已经了解到这种情况是怎么引起条件竞争,以及如何给细粒度线程安全数据结构带来麻烦。不过,非线程安全栈将top()和pop()操作分开是有意义的,当多线程会并发的访问这个栈,问题会马上出现,因为在两个操作的调用间,内部互斥锁已经被释放,并且另一个线程对栈进行了修改。解决方案就是将两个操作合并,就能用同一个锁来对操作的执行进行保护,就消除了条件竞争的问题。 42 | 43 | OK,你已经审阅过代码了(或者让别人看过)。现在,你确信代码没有问题。 44 | 45 | 就像需要用味觉来证明,你现在吃的东西——怎么测试才能确认你的代码没有bug呢? 46 | 47 | ## 10.2.2 通过测试定位并发相关的错误 48 | 49 | 写单线程应用时,如果时间充足,测试起来相对简单。原则上,设置各种可能的输入(或设置成感兴趣的情况),然后执行应用。如果应用行为和输出正确,就能判断其能对给定输入集给出正确的答案。检查错误状态(比如:处理磁盘满载错误)就会比处理可输入测试复杂的多,不过原理是一样的——设置初始条件,然后让程序执行。 50 | 51 | 测试多线程代码的难度就要比单线程大好几个数量级,因为不确定是线程的调度情况。因此,即使使用测试单线程的输入数据,如果有条件变量潜藏在代码中,那么代码的结果可能会时对时错。只是因为条件变量可能会在有些时候,等待其他事情,从而导致结果错误或正确。 52 | 53 | 因为与并发相关的bug相当难判断,所以在设计并发代码时需要格外谨慎。设计的时候,每段代码都需要进行测试,以保证没有问题,这样才能在测试出现问题的时候,剔除并发相关的bug——例如,对队列的push和pop,分别进行并发的测试,就要好于直接使用队列测试其中全部功能。这种思想能帮你在设计代码的时候,考虑什么样的代码是可以用来测试正在设计的这个结构——本章后续章节中看到与设计测试代码相关的内容。 54 | 55 | 测试的目的就是为了消除与并发相关的问题。如果在单线程测试的时候,遇到了问题,那这个问题就是普通的bug,而非并发相关的bug。当问题发生在*未测试区域*(in the wild),也就是没有在测试范围之内,像这样的情况就要特别注意。bug出现在应用的多线程部分,并不意味着该问题是一个多线程相关的bug。使用线程池管理某一级并发的时候,通常会有一个可配置的参数,用来指定工作线程的数量。当手动管理线程时,就需要将代码改成单线程的方式进行测试。不管哪种方式,将多线程简化为单线程后,就能将与多线程相关的bug排除掉。反过来说,当问题在单芯系统中消失(即使还是以多线程方式),不过问题在多芯系统或多核系统中出现,就能确定你被多线程相关的bug坑了,可能是条件变量的问题,还有可能是同步或内存序的问题。 56 | 57 | 测试并发的代码很多,不过通过测试的代码结构就没那么多了;对结构的测试也很重要,就像对环境的测试一样。 58 | 59 | 如果你依旧将测试并发队列当做一个测试例,你就需要考虑这些情况: 60 | 61 | - 使用单线程调用push()或pop(),来确定在一般情况下队列是否工作正常 62 | 63 | - 其他线程调用pop()时,使用另一线程在空队列上调用push() 64 | 65 | - 在空队列上,以多线程的方式调用push() 66 | 67 | - 在满载队列上,以多线程的方式调用push() 68 | 69 | - 在空队列上,以多线程的方式调用pop() 70 | 71 | - 在满载队列上,以多线程的方式调用pop() 72 | 73 | - 在非满载队列上(任务数量小于线程数量),以多线程的方式调用pop() 74 | 75 | - 当一线程在空队列上调用pop()的同时,以多线程的方式调用push() 76 | 77 | - 当一线程在满载队列上调用pop()的同时,以多线程的方式调用push() 78 | 79 | - 当多线程在空队列上调用pop()的同时,以多线程方式调用push() 80 | 81 | - 当多线程在满载队列上调用pop()的同时,以多线程方式调用push() 82 | 83 | 这是我所能想到的场景,可能还有更多,之后你需要考虑测试环境的因素: 84 | 85 | - “多线程”是有多少个线程(3个,4个,还是1024个?) 86 | 87 | - 系统中是否有足够的处理器,能让每个线程运行在属于自己的处理器上 88 | 89 | - 测试需要运行在哪种处理器架构上 90 | 91 | - 在测试中如何对“同时”进行合理的安排 92 | 93 | 这些因素的考虑会具体到一些特殊情况。四个因素都需要考虑,第一个和最后一个会影响测试结构本身(在10.2.5节中会介绍),另外两个就和实际的物理测试环境相关了。使用线程数量相关的测试代码需要独立测试,可通过很多结构化测试获得最合适的调度方式。在了解这些技巧前,先来了解一下如何让你的应用更容易测试。 94 | 95 | ## 10.2.3 可测试性设计 96 | 97 | 测试多线程代码很困难,所以你需要将其变得简单一些。很重要的一件事就是,在设计代码时,考虑其的可测试性。可测试的单线程代码设计已经说烂了,而且其中许多建议,在现在依旧适用。通常,如果代码满足一下几点,就很容易进行测试: 98 | 99 | - 每个函数和类的关系都很清楚。 100 | 101 | - 函数短小精悍。 102 | 103 | - 测试用例可以完全控制被测试代码周边的环境。 104 | 105 | - 执行特定操作的代码应该集中测试,而非分布式测试。 106 | 107 | - 需要在完成编写后,考虑如何进行测试。 108 | 109 | 以上这些在多线程代码中依旧适用。实际上,我会认为对多线程代码的可测试性要比单线程的更为重要,因为多线程的情况更加复杂。最后一个因素尤为重要:即使不在写完代码后,去写测试用例,这也是一个很好的建议,能让你在写代码之前,想想应该怎么去测试它——用什么作为输入,什么情况看起来会让结果变得糟糕,以及如何激发代码中潜在的问题,等等。 110 | 111 | 并发代码测试的一种最好的方式:去并发化测试。如果代码在线程间的通讯路径上出现问,就可以让一个已通讯的单线程进行执行,这样会减小问题的难度。在对数据进行访问的应用进行测试时,可以使用单线程的方式进行。这样线程通讯和对特定数据块进行访问时只有一个线程,就达到了更容易测试的目的。 112 | 113 | 例如,当应用设计为一个多线程状态机时,可以将其分为若干块。将每个逻辑状态分开,就能保证对于每个可能的输入事件、转换或其他操作的结果是正确的;这就是使用了单线程测试的技巧,测试用例提供的输入事件将来自于其他线程。之后,核心状态机和消息路由的代码,就能保证时间能以正确的顺序,正确的传递给可单独测试的线程上,不过对于多并发线程,需要为测试专门设计简单的逻辑状态。 114 | 115 | 或者,如果将代码分割成多个块(比如:读共享数据/变换数据/更新共享数据),就能使用单线程来测试变换数据的部分。麻烦的多线程测试问题,转换成单线程测试读和更新共享数据,就会简单许多。 116 | 117 | 一件事需要小心,就是某些库会用其内部变量存储状态,当多线程使用同一库中的函数,这个状态就会被共享。这的确是一个问题,并且这个问题不会马上出现在访问共享数据的代码中。不过,随着你对这个库的熟悉,就会清楚这样的情况会在什么时候出现。之后,可以适当的加一些保护和同步,或使用B计划——让多线程安全并发访问的功能。 118 | 119 | 将并发代码设计的有更好的测试性,要比以代码分块的方式处理并发相关的问题好很多。当然,还要注意对非线程安全库的调用。10.2.1节中那些问题,也需要在审阅自己代码的时候格外注意。虽然,这些问题和测试(可测试性)没有直接的关系,但带上“测试帽子”时候,就要考虑这些问题了,并且还要考虑如何测试已写好的代码,这就会影响设计方向的选择,也会让测试做的更加容易一些。 120 | 121 | 我们已经了解了如何能让测试变得更加简单,以及将代码分成一些“并发”块(比如,线程安全容器或事件逻辑状态机)以“单线程”的形式(可能还通过并发块和其他线程进行互动)进行测试。 122 | 123 | 下面就让我们了解一下测试多线程代码的技术。 124 | 125 | ## 10.2.4 多线程测试技术 126 | 127 | 想通过一些技巧写一些较短的代码,来对函数进行测试,比如:如何处理调度序列上的bug? 128 | 129 | 这里的确有几个方法能进行测试,让我们从蛮力测试(或称压力测试)开始。 130 | 131 | **蛮力测试** 132 | 133 | 代码有问题的时候,就要求蛮力测试一定能看到这个错误。这就意味着代码要运行很多遍,可能会有很多线程在同一时间运行。要是有bug出现,只能线程出现特殊调度的时候;代码运行次数的增加,就意味着bug出现的次数会增多。当有几次代码测试通过,你可能会对代码的正确性有一些信心。如果连续运行10次都通过,你就会更有信心。如果你运行十亿次都通过了,那么你就会认为这段代码没有问题了。 134 | 135 | 自信的来源是每次测试的结果。如果你的测试粒度很细,就像测试之前的线程安全队列,那么蛮力测试会让你对这段代码持有高度的自信。另一方面,当测试对象体积较大的时候,调度序列将会很长,即使运行了十亿次测试用例,也不让你对这段代码产生什么信心。 136 | 137 | 蛮力测试的缺点就是,可能会误导你。如果写出来的测试用例就为了不让有问题的情况发生,那么怎么运行,测试都不会失败,可能会因环境的原因,出现几次失败的情况。最糟糕的情况就是,问题不会出现在你的测试系统中,因为在某些特殊的系统中,这段代码就会出现问题。除非代码运行在与测试机系统相同的系统中,不过特殊的硬件和操作系统的因素结合起来,可能就会让运行环境与测试环境有所不同,问题可能就会随之出现。 138 | 139 | 这里有一个经典的案例,在单处理器系统上测试多线程应用。因为每个线程都在同一个处理器上运行,任何事情都是串行的,并且还有很多条件竞争和乒乓缓存,这些问题可能在真正的多处理器系统中,根本不会出现。还有其他变数:不同处理器架构提供不同的的同步和内存序机制。比如,在x86和x86-64架构上,原子加载操作通常是相同的,无论是使用memory_order_relaxed,还是memory_order_seq_cst(详见5.3.3节)。这就意味着在x86架构上使用松散内存序没有问题,但在有更精细的内存序指令集的架构(比如:SPARC)下,这样使用就可能产生错误。 140 | 141 | 如果你希望你的应用能跨平台使用,就要在相关的平台上进行测试。这就是我把处理器架构也列在测试需要考虑的清单中的原因(详见10.2.2)。 142 | 143 | 要避免误导的产生,关键点在于成功的蛮力测试。这就需要进行仔细考虑和设计,不仅仅是选择相关单元测试,还要遵守测试系统设计准则,以及选定测试环境。保证代码分支被尽可能的测试到,尽可能多的测试线程间的互相作用。还有,需要知道哪部分被测试覆盖到,哪些没有覆盖。 144 | 145 | 虽然,蛮力测试能够给你一些信心,不过其不保证能找到所有的问题。如果有时间将下面的技术应用到你的代码或软件中,就能保证所有的问题都能被找到。 146 | 147 | **组合仿真测试** 148 | 149 | 名字比较口语化,我需要解释一下这个测试是什么意思:使用一种特殊的软件,用来模拟代码运行的真实情况。你应该知道这种软件,能让一台物理机上运行多个虚拟环境或系统环境,而硬件环境则由监控软件来完成。除了环境是模拟的以外,模拟软件会记录对数据序列访问,上锁,以及对每个线程的原子操作。然后使用C++内存模型的规则,重复的运行,从而识别条件竞争和死锁。 150 | 151 | 虽然,这种组合测试可以保证所有与系统相关的问题都会被找到,不过过于零碎的程序将会在这种测试中耗费太长时间,因为组合数目和执行的操作数量将会随线程的增多呈指数增长态势。这个测试最好留给需要细粒度测试的代码段,而非整个应用。另一个缺点就是,代码对操作的处理,往往会依赖与模拟软件的可用性。 152 | 153 | 所以,测试需要在正常情况下,运行很多次,不过这样可能会错过一些问题;也可以在一些特殊情况下运行多次,不过这样更像是为了验证某些问题。 154 | 155 | 还有其他的测试选项吗? 156 | 157 | 第三个选项就是使用一个库,在运行测试的时候,检查代码中的问题。 158 | 159 | **使用专用库对代码进行测试** 160 | 161 | 虽然,这个选择不会像组合仿真的方式提供彻底的检查,不过可以通过特别实现的库(使用同步原语)来发现一些问题,比如:互斥量,锁和条件变量。例如,访问某块公共数据的时候,就要将指定的互斥量上锁。数据被访问后,发现一些互斥量已经上锁,就需要确定相关的互斥量是否被访问线程锁住;如果没有,测试库将报告这个错误。当需要测试库对某块代码进行检查时,可以对对应的共享数据进行标记。 162 | 163 | 当不止一个互斥量同时被一个线程持有,测试库也会对锁的序列进行记录。如果其他线程以不同的顺序进行上锁,即使在运行的时候测试用例没有发生死锁,测试库都会将这个行为记录为“有潜在死锁”可能。 164 | 165 | 当测试多线程代码的时候,另一种库可能会用到,以线程原语实现的库,比如:互斥量和条件变量;当多线程代码在等待,或是被条件变量通过notify_one()提醒的某个线程,测试者可以通过线程,获取到锁。就可以让你来安排一些特殊的情况,以验证代码是否会在这些特定的环境下产生期望的结果。 166 | 167 | C++标准库实现中,某些测试工具已经存在于标准库中,没有实现的测试工具,可以基于标准库进行实现。 168 | 169 | 了解完各种运行测试代码的方式,将让我们来了解一下,如何以你想要的调度方式来构建代码。 170 | 171 | ## 10.2.5 构建多线程测试代码 172 | 173 | 10.2.2节中提过,需要找一种合适的调度方式来处理测试中“同时”的部分,现在就是解决这个问题的时候。 174 | 175 | 在特定时间内,你需要安排一系列线程,同时去执行指定的代码段。最简单的情况:两个线程的情况,就很容易扩展到多个线程。 176 | 177 | 首先,你需要知道每个测试的不同之处: 178 | 179 | - 环境布置代码,必须首先执行 180 | 181 | - 线程设置代码,需要在每个线程上执行 182 | 183 | - 线程上执行的代码,需要有并发性 184 | 185 | - 在并发执行结束后,后续代码需要对代码的状态进行断言检查 186 | 187 | 这几条后面再解释,先让我们考虑一下10.2.2节中的一个特殊的情况:一个线程在空队列上调用push(),同时让其他线程调用pop()。 188 | 189 | 通常,布置环境的代码比较简单:创建队列即可。线程在执行pop()的时候,没有线程设置代码。线程设置代码是在执行push()操作的线程上进行的,其依赖与队列的接口和对象的存储类型。如果存储的对象需要很大的开销才能构建,或必须在堆上分配的对象,那么最好在线程设置代码中进行构建或分配;这样,就不会影响到测试结果。另外,如果队列中只存简单的int类型对象,构建int对象时就不会有太多额外的开销。实际上,已测试代码相对简单——一个线程调用push(),另一个线程调用pop()——那么,“完成后”的代码到底是什么样子呢? 190 | 191 | 在这个例子中,pop()具体做的事情,会直接影响“完成后”代码。如果有数据块,返回的肯定就是数据了,push()操作就成功的向队列中推送了一块数据,并在在数据返回后,队列依旧是空的。如果pop()没有返回数据块,也就是队列为空的情况下,操作也能执行,这样就需要两个方向的测试:要不pop()返回push()推送到队列中的数据块,之后队列依旧为空;要不pop()会示意队列中没有元素,但同时push()向队列推送了一个数据块。这两种情况都是真实存在的;你需要避免的情况是:pop()示意队列中没有数据的同时,队列还是空的,或pop()返回数据块的同时,队列中还有数据块。为了简化测试,可以假设pop()是可阻塞的。在最终代码中,需要用断言判断弹出的数据与推入的数据,还要判断队列为空。 192 | 193 | 现在,了解了各个代码块,就需要保证所有事情按计划进行。一种方式是使用一组`std::promise`来表示就绪状态。每个线程使用一个promise来表示是否准备好,然后让`std::promise`等待(复制)一个`std::shared_future`;主线程会等待每个线程上的promise设置后,才按下“开始”键。这就能保证每个线程能够同时开始,并且在准备代码执行完成后,并发代码就可以开始执行了;任何的线程特定设置都需要在设置线程的promise前完成。最终,主线程会等待所有线程完成,并且检查其最终状态。还需要格外关心的是——异常,所有线程在准备好的情况下,才按下“开始”键;否则,未准备好的线程就不会运行。 194 | 195 | 下面的代码,构建了这样的测试。 196 | 197 | 清单10.1 对一个队列并发调用push()和pop()的测试用例 198 | ``` 199 | void test_concurrent_push_and_pop_on_empty_queue() 200 | { 201 | threadsafe_queue q; // 1 202 | 203 | std::promise go,push_ready,pop_ready; // 2 204 | std::shared_future ready(go.get_future()); // 3 205 | 206 | std::future push_done; // 4 207 | std::future pop_done; 208 | 209 | try 210 | { 211 | push_done=std::async(std::launch::async, // 5 212 | [&q,ready,&push_ready]() 213 | { 214 | push_ready.set_value(); 215 | ready.wait(); 216 | q.push(42); 217 | } 218 | ); 219 | pop_done=std::async(std::launch::async, // 6 220 | [&q,ready,&pop_ready]() 221 | { 222 | pop_ready.set_value(); 223 | ready.wait(); 224 | return q.pop(); // 7 225 | } 226 | ); 227 | push_ready.get_future().wait(); // 8 228 | pop_ready.get_future().wait(); 229 | go.set_value(); // 9 230 | 231 | push_done.get(); // 10 232 | assert(pop_done.get()==42); // 11 233 | assert(q.empty()); 234 | } 235 | catch(...) 236 | { 237 | go.set_value(); // 12 238 | throw; 239 | } 240 | } 241 | ``` 242 | 243 | 首先,环境设置代码中创建了空队列①。然后,为准备状态创建promise对象②,并且为go信号获取一个`std::shared_future`对象③。再后,创建了future用来表示线程是否结束④。这些都需要放在try块外面,再设置go信号时抛出异常,就不需要等待其他城市线程完成任务了(这将会产生死锁——如果测试代码产生死锁,测试代码就是不理想的代码)。 244 | 245 | try块中可以启动线程⑤⑥——使用`std::launch::async`保证每个任务在自己的线程上完成。注意,使用`std::async`会让你任务更容易成为线程安全的任务;这里不用普通`std::thread`,因为其析构函数会对future进行线程汇入。lambda函数会捕捉指定的任务(会在队列中引用),并且为promise准备相关的信号,同时对从go中获取的ready做一份拷贝。 246 | 247 | 如之前所说,每个任务集都有自己的ready信号,并且会在执行测试代码前,等待所有的ready信号。而主线程不同——等待所有线程的信号前⑧,提示所有线程可以开始进行测试了⑨。 248 | 249 | 最终,异步调用等待线程完成后⑩⑪,主线程会从中获取future,再调用get()成员函数获取结果,最后对结果进行检查。注意,这里pop操作通过future返回检索值⑦,所以能获取最终的结果⑪。 250 | 251 | 当有异常抛出,需要通过对go信号的设置来避免悬空指针的产生,再重新抛出异常⑫。future与之后声明的任务相对应④,所以future将会被首先销毁,如果future都没有就绪,析构函数将会等待相关任务完成后执行操作。 252 | 253 | 虽然,像是使用测试模板对两个调用进行测试,但使用类似的东西是必要的,这样会便于测试的进行。例如,启动一个线程就是一个很耗时的过程,如果没有线程在等待go信号时,推送线程可能会在弹出线程开始之前,就已经完成了;这样就失去了测试的作用。以这种方式使用future,就是为了保证线程都在运行,并且阻塞在同一个future上。future解除阻塞后,将会让所有线程运行起来。当你熟悉了这个结构,其就能以同样的模式创建新的测试用例。测试两个以上的线程,这种模式很容易进行扩展。 254 | 255 | 目前,我们已经了解了多线程代码的正确性测试。 256 | 257 | 虽然这是最最重要的问题,但是其不是我们做测试的唯一原因:多线程性能的测试同样重要。 258 | 259 | 下面就让我们来了解一下性能测试。 260 | 261 | ## 10.2.6 测试多线程代码性能 262 | 263 | 选择以并发的方式开发应用,就是为了能够使用日益增长的处理器数量;通过处理器数量的增加,来提升应用的执行效率。因此,确定性能是否有真正的提高就很重要了(就像其他优化一样)。 264 | 265 | 并发效率中有个特别的问题——可扩展性——你希望代码能很快的运行24次,或在24芯的机器上对数据进行24(或更多)次处理,或其他等价情况。你不会希望,你的代码运行两次的数据和在双芯机器上执行一样快的同时,在24芯的机器上会更慢。如8.4.2节中所述,当有重要的代码以单线程方式运行时,就会限制性能的提高。因此,在做测试之前,回顾一下代码的设计结构是很有必要的;这样就能判断,代码在24芯的机器上时,性能会不会提高24倍,或是因为有串行部分的存在,最大的加速比只有3。 266 | 267 | 在对数据访问的时候,处理器之间会有竞争,会对性能有很大的影响。需要合理的权衡性能和处理器的数量,处理器数量太少,就会等待很久;处理器过多,又会因为竞争的原因等待很久。 268 | 269 | 因此,在对应的系统上通过不同的配置,检查多线程的性能就很有必要,这样可以得到一张性能伸缩图。最起码,(如果条件允许)你应该在一个单处理器的系统上和一个多处理核芯的系统上进行测试。 -------------------------------------------------------------------------------- /content/chapter10/10.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 10.3 本章总结 2 | 3 | 本章我们了解了各种与并发相关的bug,从死锁和活锁,再到数据竞争和其他恶性条件竞争;我们也使用了一些技术来定位bug。同样,也讨论了在做代码审阅的时候需做哪些思考,以及写可测试代码的指导意见,还有如何为并发代码构造测试用例。最终,我们还了解了一些对测试很有帮助的工具。 -------------------------------------------------------------------------------- /content/chapter2/2.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第2章 线程管理 2 | 3 | **本章主要内容** 4 | 5 | - 启动新线程
6 | - 等待线程与分离线程
7 | - 线程唯一标识符
8 | 9 | 好的!看来你已经决定使用多线程了。先做点什么呢?启动线程、结束线程,还是如何监管线程?C++标准库中只需要管理`std::thread`关联的线程,无需把注意力放在其他方面。不过,标准库太灵活,所以管理起来不会太容易。 10 | 11 | 本章将从基本开始:启动一个线程,等待这个线程结束,或放在后台运行。再看看怎么给已经启动的线程函数传递参数,以及怎么将一个线程的所有权从当前`std::thread`对象移交给另一个。最后,再来确定线程数,以及识别特殊线程。 -------------------------------------------------------------------------------- /content/chapter2/2.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 2.1 线程管理的基础 2 | 3 | 每个程序至少有一个线程:执行main\(\)函数的线程,其余线程有其各自的入口函数。线程与原始线程\(以main\(\)为入口函数的线程\)同时运行。如同main\(\)函数执行完会退出一样,当线程执行完入口函数后,线程也会退出。在为一个线程创建了一个`std::thread`对象后,需要等待这个线程结束;不过,线程需要先进行启动。下面就来启动线程。 4 | 5 | ## 2.1.1 启动线程 6 | 7 | 第1章中,线程在`std::thread`对象创建\(为线程指定任务\)时启动。最简单的情况下,任务也会很简单,通常是无参数无返回的函数。这种函数在其所属线程上运行,直到函数执行完毕,线程也就结束了。在一些极端情况下,线程运行时,任务中的函数对象需要通过某种通讯机制进行参数的传递,或者执行一系列独立操作;可以通过通讯机制传递信号,让线程停止。线程要做什么,以及什么时候启动,其实都无关紧要。总之,使用C++线程库启动线程,可以归结为构造`std::thread`对象: 8 | 9 | ``` 10 | void do_some_work(); 11 | std::thread my_thread(do_some_work); 12 | ``` 13 | 14 | 为了让编译器识别`std::thread`类,这个简单的例子也要包含``头文件。如同大多数C++标准库一样,`std::thread`可以用可调用类型构造,将带有函数调用符类型的实例传入`std::thread`类中,替换默认的构造函数。 15 | 16 | ``` 17 | class background_task 18 | { 19 | public: 20 | void operator()() const 21 | { 22 | do_something(); 23 | do_something_else(); 24 | } 25 | }; 26 | 27 | background_task f; 28 | std::thread my_thread(f); 29 | ``` 30 | 31 | 代码中,提供的函数对象会复制到新线程的存储空间当中,函数对象的执行和调用都在线程的内存空间中进行。函数对象的副本应与原始函数对象保持一致,否则得到的结果会与我们的期望不同。 32 | 33 | 有件事需要注意,当把函数对象传入到线程构造函数中时,需要避免“[最令人头痛的语法解析](http://en.wikipedia.org/wiki/Most_vexing_parse)”\(_C++’s most vexing parse_, [中文简介](http://qiezhuifeng.diandian.com/post/2012-08-27/40038339477)\)。如果你传递了一个临时变量,而不是一个命名的变量;C++编译器会将其解析为函数声明,而不是类型对象的定义。 34 | 35 | 例如: 36 | 37 | ``` 38 | std::thread my_thread(background_task()); 39 | ``` 40 | 41 | 这里相当与声明了一个名为my\_thread的函数,这个函数带有一个参数\(函数指针指向没有参数并返回background\_task对象的函数\),返回一个`std::thread`对象的函数,而非启动了一个线程。 42 | 43 | 使用在前面命名函数对象的方式,或使用多组括号①,或使用新统一的初始化语法②,可以避免这个问题。 44 | 45 | 如下所示: 46 | 47 | ``` 48 | std::thread my_thread((background_task())); // 1 49 | std::thread my_thread{background_task()}; // 2 50 | ``` 51 | 52 | 使用lambda表达式也能避免这个问题。lambda表达式是C++11的一个新特性,它允许使用一个可以捕获局部变量的局部函数\(可以避免传递参数,参见2.2节\)。想要具体的了解lambda表达式,可以阅读附录A的A.5节。之前的例子可以改写为lambda表达式的类型: 53 | 54 | ``` 55 | std::thread my_thread([]{ 56 | do_something(); 57 | do_something_else(); 58 | }); 59 | ``` 60 | 61 | 启动了线程,你需要明确是要等待线程结束\(_加入式_——参见2.1.2节\),还是让其自主运行\(_分离式_——参见2.1.3节\)。如果`std::thread`对象销毁之前还没有做出决定,程序就会终止\(`std::thread`的析构函数会调用`std::terminate()`\)。因此,即便是有异常存在,也需要确保线程能够正确的_加入_\(joined\)或_分离_\(detached\)。2.1.3节中,会介绍对应的方法来处理这两种情况。需要注意的是,必须在`std::thread`对象销毁之前做出决定,否则你的程序将会终止\(std::thread的析构函数会调用std::terminate\(\),这时再去决定会触发相应异常\)。 62 | 63 | 如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性。这不是一个新问题——单线程代码中,对象销毁之后再去访问,也会产生未定义行为——不过,线程的生命周期增加了这个问题发生的几率。 64 | 65 | 这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引用。下面的清单中就展示了这样的一种情况。 66 | 67 | 清单2.1 函数已经结束,线程依旧访问局部变量 68 | 69 | ``` 70 | struct func 71 | { 72 | int& i; 73 | func(int& i_) : i(i_) {} 74 | void operator() () 75 | { 76 | for (unsigned j=0 ; j<1000000 ; ++j) 77 | { 78 | do_something(i); // 1. 潜在访问隐患:悬空引用 79 | } 80 | } 81 | }; 82 | 83 | void oops() 84 | { 85 | int some_local_state=0; 86 | func my_func(some_local_state); 87 | std::thread my_thread(my_func); 88 | my_thread.detach(); // 2. 不等待线程结束 89 | } // 3. 新线程可能还在运行 90 | ``` 91 | 92 | 这个例子中,已经决定不等待线程结束\(使用了detach\(\)②\),所以当oops\(\)函数执行完成时③,新线程中的函数可能还在运行。如果线程还在运行,它就会去调用do\_something\(i\)函数①,这时就会访问已经销毁的变量。如同一个单线程程序——允许在函数完成后继续持有局部变量的指针或引用;当然,这从来就不是一个好主意——这种情况发生时,错误并不明显,会使多线程更容易出错。 93 | 94 | 处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。如果使用一个可调用的对象作为线程函数,这个对象就会复制到线程中,而后原始对象就会立即销毁。但对于对象中包含的指针和引用还需谨慎,例如清单2.1所示。使用一个能访问局部变量的函数去创建线程是一个糟糕的主意\(除非**十分确定**线程会在函数完成前结束\)。此外,可以通过join\(\)函数来确保线程在函数完成前结束。 95 | 96 | ## 2.1.2 等待线程完成 97 | 98 | 如果需要等待线程,相关的`std::thread`实例需要使用join\(\)。清单2.1中,将`my_thread.detach()`替换为`my_thread.join()`,就可以确保局部变量在线程完成后,才被销毁。在这种情况下,因为原始线程在其生命周期中并没有做什么事,使得用一个独立的线程去执行函数变得收益甚微,但在实际编程中,原始线程要么有自己的工作要做;要么会启动多个子线程来做一些有用的工作,并等待这些线程结束。 99 | 100 | join\(\)是简单粗暴的等待线程完成或不等待。当你需要对等待中的线程有更灵活的控制时,比如,看一下某个线程是否结束,或者只等待一段时间\(超过时间就判定为超时\)。想要做到这些,你需要使用其他机制来完成,比如条件变量和_期待_\(futures\),相关的讨论将会在第4章继续。调用join\(\)的行为,还清理了线程相关的存储部分,这样`std::thread`对象将不再与已经完成的线程有任何关联。这意味着,只能对一个线程使用一次join\(\);一旦已经使用过join\(\),`std::thread`对象就不能再次加入了,当对其使用joinable\(\)时,将返回false。 101 | 102 | ## 2.1.3 特殊情况下的等待 103 | 104 | 如前所述,需要对一个还未销毁的`std::thread`对象使用join\(\)或detach\(\)。如果想要分离一个线程,可以在线程启动后,直接使用detach\(\)进行分离。如果打算等待对应线程,则需要细心挑选调用join\(\)的位置。当在线程运行之后产生异常,在join\(\)调用之前抛出,就意味着这次调用会被跳过。 105 | 106 | 避免应用被抛出的异常所终止,就需要作出一个决定。通常,当倾向于在无异常的情况下使用join\(\)时,需要在异常处理过程中调用join\(\),从而避免生命周期的问题。下面的程序清单是一个例子。 107 | 108 | 清单 2.2 等待线程完成 109 | 110 | ``` 111 | struct func; // 定义在清单2.1中 112 | void f() 113 | { 114 | int some_local_state=0; 115 | func my_func(some_local_state); 116 | std::thread t(my_func); 117 | try 118 | { 119 | do_something_in_current_thread(); 120 | } 121 | catch(...) 122 | { 123 | t.join(); // 1 124 | throw; 125 | } 126 | t.join(); // 2 127 | } 128 | ``` 129 | 130 | 清单2.2中的代码使用了`try/catch`块确保访问本地状态的线程退出后,函数才结束。当函数正常退出时,会执行到②处;当函数执行过程中抛出异常,程序会执行到①处。`try/catch`块能轻易的捕获轻量级错误,所以这种情况,并非放之四海而皆准。如需确保线程在函数之前结束——查看是否因为线程函数使用了局部变量的引用,以及其他原因——而后再确定一下程序可能会退出的途径,无论正常与否,可以提供一个简洁的机制,来做解决这个问题。 131 | 132 | 一种方式是使用“资源获取即初始化方式”\(RAII,Resource Acquisition Is Initialization\),并且提供一个类,在析构函数中使用**join\(\)**,如同下面清单中的代码。看它如何简化f\(\)函数。 133 | 134 | 清单 2.3 使用RAII等待线程完成 135 | 136 | ``` 137 | class thread_guard 138 | { 139 | std::thread& t; 140 | public: 141 | explicit thread_guard(std::thread& t_): 142 | t(t_) 143 | {} 144 | ~thread_guard() 145 | { 146 | if(t.joinable()) // 1 147 | { 148 | t.join(); // 2 149 | } 150 | } 151 | thread_guard(thread_guard const&)=delete; // 3 152 | thread_guard& operator=(thread_guard const&)=delete; 153 | }; 154 | 155 | struct func; // 定义在清单2.1中 156 | 157 | void f() 158 | { 159 | int some_local_state=0; 160 | func my_func(some_local_state); 161 | std::thread t(my_func); 162 | thread_guard g(t); 163 | do_something_in_current_thread(); 164 | } // 4 165 | ``` 166 | 167 | 当线程执行到④处时,局部对象就要被逆序销毁了。因此,thread\_guard对象g是第一个被销毁的,这时线程在析构函数中被加入②到原始线程中。即使do\_something\_in\_current\_thread抛出一个异常,这个销毁依旧会发生。 168 | 169 | 在thread\_guard的析构函数的测试中,首先判断线程是否已加入①,如果没有会调用join\(\)②进行加入。这很重要,因为join\(\)只能对给定的对象调用一次,所以对给已加入的线程再次进行加入操作时,将会导致错误。 170 | 171 | 拷贝构造函数和拷贝赋值操作被标记为`=delete`③,是为了不让编译器自动生成它们。直接对一个对象进行拷贝或赋值是危险的,因为这可能会弄丢已经加入的线程。通过删除声明,任何尝试给thread\_guard对象赋值的操作都会引发一个编译错误。想要了解删除函数的更多知识,请参阅附录A的A.2节。 172 | 173 | 如果不想等待线程结束,可以_分离_\(_detaching\)线程,从而避免_异常安全\*\(exception-safety\)问题。不过,这就打破了线程与`std::thread`对象的联系,即使线程仍然在后台运行着,分离操作也能确保`std::terminate()`在`std::thread`对象销毁才被调用。 174 | 175 | ## 2.1.4 后台运行线程 176 | 177 | 使用detach\(\)会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有`std::thread`对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。 178 | 179 | 通常称分离线程为_守护线程_\(daemon threads\),UNIX中守护线程是指,没有任何显式的用户接口,并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,_发后即忘_\(fire and forget\)的任务就使用到线程的这种方式。 180 | 181 | 如2.1.2节所示,调用`std::thread`成员函数detach\(\)来分离一个线程。之后,相应的`std::thread`对象就与实际执行的线程无关了,并且这个线程也无法加入: 182 | 183 | ``` 184 | std::thread t(do_background_work); 185 | t.detach(); 186 | assert(!t.joinable()); 187 | ``` 188 | 189 | 为了从`std::thread`对象中分离线程\(前提是有可进行分离的线程\),不能对没有执行线程的`std::thread`对象使用detach\(\),也是join\(\)的使用条件,并且要用同样的方式进行检查——当`std::thread`对象使用t.joinable\(\)返回的是true,就可以使用t.detach\(\)。 190 | 191 | 试想如何能让一个文字处理应用同时编辑多个文档。无论是用户界面,还是在内部应用内部进行,都有很多的解决方法。虽然,这些窗口看起来是完全独立的,每个窗口都有自己独立的菜单选项,但他们却运行在同一个应用实例中。一种内部处理方式是,让每个文档处理窗口拥有自己的线程;每个线程运行同样的的代码,并隔离不同窗口处理的数据。如此这般,打开一个文档就要启动一个新线程。因为是对独立的文档进行操作,所以没有必要等待其他线程完成。因此,这里就可以让文档处理窗口运行在分离的线程上。 192 | 193 | 下面代码简要的展示了这种方法: 194 | 195 | 清单2.4 使用分离线程去处理其他文档 196 | 197 | ``` 198 | void edit_document(std::string const& filename) 199 | { 200 | open_document_and_display_gui(filename); 201 | while(!done_editing()) 202 | { 203 | user_command cmd=get_user_input(); 204 | if(cmd.type==open_new_document) 205 | { 206 | std::string const new_name=get_filename_from_user(); 207 | std::thread t(edit_document,new_name); // 1 208 | t.detach(); // 2 209 | } 210 | else 211 | { 212 | process_user_input(cmd); 213 | } 214 | } 215 | } 216 | ``` 217 | 218 | 如果用户选择打开一个新文档,需要启动一个新线程去打开新文档①,并分离线程②。与当前线程做出的操作一样,新线程只不过是打开另一个文件而已。所以,edit\_document函数可以复用,通过传参的形式打开新的文件。 219 | 220 | 这个例子也展示了传参启动线程的方法:不仅可以向`std::thread`构造函数①传递函数名,还可以传递函数所需的参数\(实参\)。C++线程库的方式也不是很复杂。当然,也有其他方法完成这项功能,比如:使用一个带有数据成员的成员函数,代替一个需要传参的普通函数。 221 | 222 | -------------------------------------------------------------------------------- /content/chapter2/2.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 2.2 向线程函数传递参数 2 | 3 | 清单2.4中,向`std::thread`构造函数中的可调用对象,或函数传递一个参数很简单。需要注意的是,默认参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问。再来看一个例子: 4 | 5 | ``` 6 | void f(int i, std::string const& s); 7 | std::thread t(f, 3, "hello"); 8 | ``` 9 | 10 | 代码创建了一个调用f(3, "hello")的线程。注意,函数f需要一个`std::string`对象作为第二个参数,但这里使用的是字符串的字面值,也就是`char const *`类型。之后,在线程的上下文中完成字面值向`std::string`对象的转化。需要特别要注意,当指向动态变量的指针作为参数传递给线程的情况,代码如下: 11 | 12 | ``` 13 | void f(int i,std::string const& s); 14 | void oops(int some_param) 15 | { 16 | char buffer[1024]; // 1 17 | sprintf(buffer, "%i",some_param); 18 | std::thread t(f,3,buffer); // 2 19 | t.detach(); 20 | } 21 | ``` 22 | 23 | 这种情况下,buffer②是一个指针变量,指向本地变量,然后本地变量通过buffer传递到新线程中②。并且,函数有很有可能会在字面值转化成`std::string`对象之前*崩溃*(oops),从而导致一些未定义的行为。并且想要依赖隐式转换将字面值转换为函数期待的`std::string`对象,但因`std::thread`的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。 24 | 25 | 解决方案就是在传递到`std::thread`构造函数之前就将字面值转化为`std::string`对象: 26 | 27 | ``` 28 | void f(int i,std::string const& s); 29 | void not_oops(int some_param) 30 | { 31 | char buffer[1024]; 32 | sprintf(buffer,"%i",some_param); 33 | std::thread t(f,3,std::string(buffer)); // 使用std::string,避免悬垂指针 34 | t.detach(); 35 | } 36 | ``` 37 | 38 | 还可能遇到相反的情况:期望传递一个引用,但整个对象被复制了。当线程更新一个引用传递的数据结构时,这种情况就可能发生,比如: 39 | 40 | ``` 41 | void update_data_for_widget(widget_id w,widget_data& data); // 1 42 | void oops_again(widget_id w) 43 | { 44 | widget_data data; 45 | std::thread t(update_data_for_widget,w,data); // 2 46 | display_status(); 47 | t.join(); 48 | process_widget_data(data); // 3 49 | } 50 | ``` 51 | 52 | 虽然update_data_for_widget①的第二个参数期待传入一个引用,但是`std::thread`的构造函数②并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。当线程调用update_data_for_widget函数时,传递给函数的参数是data变量内部拷贝的引用,而非数据本身的引用。因此,当线程结束时,内部拷贝数据将会在数据更新阶段被销毁,且process_widget_data将会接收到没有修改的data变量③。对于熟悉`std::bind`的开发者来说,问题的解决办法是显而易见的:可以使用`std::ref`将参数转换成引用的形式,从而可将线程的调用改为以下形式: 53 | 54 | ``` 55 | std::thread t(update_data_for_widget,w,std::ref(data)); 56 | ``` 57 | 58 | 在这之后,update_data_for_widget就会接收到一个data变量的引用,而非一个data变量拷贝的引用。 59 | 60 | 如果你熟悉`std::bind`,就应该不会对以上述传参的形式感到奇怪,因为`std::thread`构造函数和`std::bind`的操作都在标准库中定义好了,可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个参数: 61 | 62 | ```c++ 63 | class X 64 | { 65 | public: 66 | void do_lengthy_work(); 67 | }; 68 | X my_x; 69 | std::thread t(&X::do_lengthy_work,&my_x); // 1 70 | ``` 71 | 72 | 这段代码中,新线程将my_x.do_lengthy_work()作为线程函数;my_x的地址①作为指针对象提供给函数。也可以为成员函数提供参数:`std::thread`构造函数的第三个参数就是成员函数的第一个参数,以此类推(代码如下,译者自加)。 73 | 74 | ```c++ 75 | class X 76 | { 77 | public: 78 | void do_lengthy_work(int); 79 | }; 80 | X my_x; 81 | int num(0); 82 | std::thread t(&X::do_lengthy_work, &my_x, num); 83 | ``` 84 | 85 | 有趣的是,提供的参数可以*移动*,但不能*拷贝*。"移动"是指:原始对象中的数据转移给另一对象,而转移的这些数据就不再在原始对象中保存了(译者:比较像在文本编辑的"剪切"操作)。`std::unique_ptr`就是这样一种类型(译者:C++11中的智能指针),这种类型为动态分配的对象提供内存自动管理机制(译者:类似垃圾回收)。同一时间内,只允许一个`std::unique_ptr`实现指向一个给定对象,并且当这个实现销毁时,指向的对象也将被删除。*移动构造函数*(move constructor)和*移动赋值操作符*(move assignment operator)允许一个对象在多个`std::unique_ptr`实现中传递(有关"移动"的更多内容,请参考附录A的A.1.1节)。使用"移动"转移原对象后,就会留下一个*空指针*(NULL)。移动操作可以将对象转换成可接受的类型,例如:函数参数或函数返回的类型。当原对象是一个临时变量时,自动进行移动操作,但当原对象是一个命名变量,那么转移的时候就需要使用`std::move()`进行显示移动。下面的代码展示了`std::move`的用法,展示了`std::move`是如何转移一个动态对象到一个线程中去的: 86 | 87 | ```c++ 88 | void process_big_object(std::unique_ptr); 89 | 90 | std::unique_ptr p(new big_object); 91 | p->prepare_data(42); 92 | std::thread t(process_big_object,std::move(p)); 93 | ``` 94 | 95 | 在`std::thread`的构造函数中指定`std::move(p)`,big_object对象的所有权就被首先转移到新创建线程的的内部存储中,之后传递给process_big_object函数。 96 | 97 | 标准线程库中和`std::unique_ptr`在所属权上有相似语义类型的类有好几种,`std::thread`为其中之一。虽然,`std::thread`实例不像`std::unique_ptr`那样能占有一个动态对象的所有权,但是它能占有其他资源:每个实例都负责管理一个执行线程。执行线程的所有权可以在多个`std::thread`实例中互相转移,这是依赖于`std::thread`实例的*可移动*且*不可复制*性。不可复制保性证了在同一时间点,一个`std::thread`实例只能关联一个执行线程;可移动性使得程序员可以自己决定,哪个实例拥有实际执行线程的所有权。 -------------------------------------------------------------------------------- /content/chapter2/2.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 2.3 转移线程所有权 2 | 3 | 假设要写一个在后台启动线程的函数,想通过新线程返回的所有权去调用这个函数,而不是等待线程结束再去调用;或完全与之相反的想法:创建一个线程,并在函数中转移所有权,都必须要等待线程结束。总之,新线程的所有权都需要转移。 4 | 5 | 这就是移动引入`std::thread`的原因,C++标准库中有很多_资源占有_\(resource-owning\)类型,比如`std::ifstream`,`std::unique_ptr`还有`std::thread`都是可移动,但不可拷贝。这就说明执行线程的所有权可以在`std::thread`实例中移动,下面将展示一个例子。例子中,创建了两个执行线程,并且在`std::thread`实例之间\(t1,t2和t3\)转移所有权: 6 | 7 | ``` 8 | void some_function(); 9 | void some_other_function(); 10 | std::thread t1(some_function); // 1 11 | std::thread t2=std::move(t1); // 2 12 | t1=std::thread(some_other_function); // 3 13 | std::thread t3; // 4 14 | t3=std::move(t2); // 5 15 | t1=std::move(t3); // 6 赋值操作将使程序崩溃 16 | ``` 17 | 18 | 首先,新线程开始与t1相关联。当显式使用`std::move()`创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没有关联了;执行some\_function的函数现在与t2关联。 19 | 20 | 然后,与一个临时`std::thread`对象相关的线程启动了③。为什么不显式调用`std::move()`转移所有权呢?因为,所有者是一个临时对象——移动操作将会隐式的调用。 21 | 22 | t3使用默认构造方式创建④,与任何执行线程都没有关联。调用`std::move()`将与t2关联线程的所有权转移到t3中⑤。因为t2是一个命名对象,需要显式的调用`std::move()`。移动操作⑤完成后,t1与执行some\_other\_function的线程相关联,t2与任何线程都无关联,t3与执行some\_function的线程相关联。 23 | 24 | 最后一个移动操作,将some\_function线程的所有权转移⑥给t1。不过,t1已经有了一个关联的线程\(执行some\_other\_function的线程\),所以这里系统直接调用`std::terminate()`终止程序继续运行。这样做(不抛出异常,`std::terminate()`是[_noexcept_](http://www.baidu.com/link?url=5JjyAaqAzTTXfKVx1iXU2L1aR__8o4wfW4iotLW1BiUCTzDHjbGcX7Qx42FOcd0K4xe2MDFgL5r7BCiVClXCDq)函数\)是为了保证与`std::thread`的析构函数的行为一致。2.1.1节中,需要在线程对象被析构前,显式的等待线程完成,或者分离它;进行赋值时也需要满足这些条件\(说明:不能通过赋一个新值给`std::thread`对象的方式来"丢弃"一个线程\)。 25 | 26 | `std::thread`支持移动,就意味着线程的所有权可以在函数外进行转移,就如下面程序一样。 27 | 28 | 清单2.5 函数返回`std::thread`对象 29 | 30 | ``` 31 | std::thread f() 32 | { 33 | void some_function(); 34 | return std::thread(some_function); 35 | } 36 | 37 | std::thread g() 38 | { 39 | void some_other_function(int); 40 | std::thread t(some_other_function,42); 41 | return t; 42 | } 43 | ``` 44 | 45 | 当所有权可以在函数内部传递,就允许`std::thread`实例可作为参数进行传递,代码如下: 46 | 47 | ``` 48 | void f(std::thread t); 49 | void g() 50 | { 51 | void some_function(); 52 | f(std::thread(some_function)); 53 | std::thread t(some_function); 54 | f(std::move(t)); 55 | } 56 | ``` 57 | 58 | `std::thread`支持移动的好处是可以创建thread\_guard类的实例\(定义见 清单2.3\),并且拥有其线程的所有权。当thread\_guard对象所持有的线程已经被引用,移动操作就可以避免很多不必要的麻烦;这意味着,当某个对象转移了线程的所有权后,它就不能对线程进行加入或分离。为了确保线程程序退出前完成,下面的代码里定义了scoped\_thread类。现在,我们来看一下这段代码: 59 | 60 | 清单2.6 scoped\_thread的用法 61 | 62 | ``` 63 | class scoped_thread 64 | { 65 | std::thread t; 66 | public: 67 | explicit scoped_thread(std::thread t_): // 1 68 | t(std::move(t_)) 69 | { 70 | if(!t.joinable()) // 2 71 | throw std::logic_error(“No thread”); 72 | } 73 | ~scoped_thread() 74 | { 75 | t.join(); // 3 76 | } 77 | scoped_thread(scoped_thread const&)=delete; 78 | scoped_thread& operator=(scoped_thread const&)=delete; 79 | }; 80 | 81 | struct func; // 定义在清单2.1中 82 | 83 | void f() 84 | { 85 | int some_local_state; 86 | scoped_thread t(std::thread(func(some_local_state))); // 4 87 | do_something_in_current_thread(); 88 | } // 5 89 | ``` 90 | 91 | 与清单2.3相似,不过这里新线程是直接传递到scoped\_thread中④,而非创建一个独立的命名变量。当主线程到达f\(\)函数的末尾时,scoped\_thread对象将会销毁,然后加入③到的构造函数①创建的线程对象中去。而在清单2.3中的thread\_guard类,就要在析构的时候检查线程是否"可加入"。这里把检查放在了构造函数中②,并且当线程不可加入时,抛出异常。 92 | 93 | `std::thread`对象的容器,如果这个容器是移动敏感的\(比如,标准中的`std::vector<>`\),那么移动操作同样适用于这些容器。了解这些后,就可以写出类似清单2.7中的代码,代码量产了一些线程,并且等待它们结束。 94 | 95 | 清单2.7 量产线程,等待它们结束 96 | 97 | ``` 98 | void do_work(unsigned id); 99 | 100 | void f() 101 | { 102 | std::vector threads; 103 | for(unsigned i=0; i < 20; ++i) 104 | { 105 | threads.push_back(std::thread(do_work,i)); // 产生线程 106 | } 107 | std::for_each(threads.begin(),threads.end(), 108 | std::mem_fn(&std::thread::join)); // 对每个线程调用join() 109 | } 110 | ``` 111 | 112 | 我们经常需要线程去分割一个算法的总工作量,所以在算法结束的之前,所有的线程必须结束。清单2.7说明线程所做的工作都是独立的,并且结果仅会受到共享数据的影响。如果f\(\)有返回值,这个返回值就依赖于线程得到的结果。在写入返回值之前,程序会检查使用共享数据的线程是否终止。操作结果在不同线程中转移的替代方案,我们会在第4章中再次讨论。 113 | 114 | 将`std::thread`放入`std::vector`是向线程自动化管理迈出的第一步:并非为这些线程创建独立的变量,并且将他们直接加入,可以把它们当做一个组。创建一组线程\(数量在运行时确定\),可使得这一步迈的更大,而非像清单2.7那样创建固定数量的线程。 115 | 116 | -------------------------------------------------------------------------------- /content/chapter2/2.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 2.4 运行时决定线程数量 2 | 3 | `std::thread::hardware_concurrency()`在新版C++标准库中是一个很有用的函数。这个函数将返回能同时并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这也无法掩盖这个函数对启动线程数量的帮助。 4 | 5 | 清单2.8实现了一个并行版的`std::accumulate`。代码中将整体工作拆分成小任务交给每个线程去做,其中设置最小任务数,是为了避免产生太多的线程。程序可能会在操作数量为0的时候抛出异常。比如,`std::thread`构造函数无法启动一个执行线程,就会抛出一个异常。在这个算法中讨论异常处理,已经超出现阶段的讨论范围,这个问题我们将在第8章中再来讨论。 6 | 7 | 清单2.8 原生并行版的`std::accumulate` 8 | 9 | ``` 10 | template 11 | struct accumulate_block 12 | { 13 | void operator()(Iterator first,Iterator last,T& result) 14 | { 15 | result=std::accumulate(first,last,result); 16 | } 17 | }; 18 | 19 | template 20 | T parallel_accumulate(Iterator first,Iterator last,T init) 21 | { 22 | unsigned long const length=std::distance(first,last); 23 | 24 | if(!length) // 1 25 | return init; 26 | 27 | unsigned long const min_per_thread=25; 28 | unsigned long const max_threads= 29 | (length+min_per_thread-1)/min_per_thread; // 2 30 | 31 | unsigned long const hardware_threads= 32 | std::thread::hardware_concurrency(); 33 | 34 | unsigned long const num_threads= // 3 35 | std::min(hardware_threads != 0 ? hardware_threads : 2, max_threads); 36 | 37 | unsigned long const block_size=length/num_threads; // 4 38 | 39 | std::vector results(num_threads); 40 | std::vector threads(num_threads-1); // 5 41 | 42 | Iterator block_start=first; 43 | for(unsigned long i=0; i < (num_threads-1); ++i) 44 | { 45 | Iterator block_end=block_start; 46 | std::advance(block_end,block_size); // 6 47 | threads[i]=std::thread( // 7 48 | accumulate_block(), 49 | block_start,block_end,std::ref(results[i])); 50 | block_start=block_end; // 8 51 | } 52 | accumulate_block()( 53 | block_start,last,results[num_threads-1]); // 9 54 | std::for_each(threads.begin(),threads.end(), 55 | std::mem_fn(&std::thread::join)); // 10 56 | 57 | return std::accumulate(results.begin(),results.end(),init); // 11 58 | } 59 | ``` 60 | 61 | 函数看起来很长,但不复杂。如果输入的范围为空①,就会得到init的值。反之,如果范围内多于一个元素时,都需要用范围内元素的总数量除以线程(块)中最小任务数,从而确定启动线程的最大数量②,这样能避免无谓的计算资源的浪费。比如,一台32芯的机器上,只有5个数需要计算,却启动了32个线程。 62 | 63 | 计算量的最大值和硬件支持线程数中,较小的值为启动线程的数量③。因为上下文频繁的切换会降低线程的性能,所以你肯定不想启动的线程数多于硬件支持的线程数量。当`std::thread::hardware_concurrency()`返回0,你可以选择一个合适的数作为你的选择;在本例中,我选择了"2"。你也不想在一台单核机器上启动太多的线程,因为这样反而会降低性能,有可能最终让你放弃使用并发。 64 | 65 | 每个线程中处理的元素数量,是范围中元素的总量除以线程的个数得出的④。对于分配是否得当,我们会在后面讨论。 66 | 67 | 现在,确定了线程个数,通过创建一个`std::vector`容器存放中间结果,并为线程创建一个`std::vector`容器⑤。这里需要注意的是,启动的线程数必须比num_threads少1个,因为在启动之前已经有了一个线程(主线程)。 68 | 69 | 使用简单的循环来启动线程:block_end迭代器指向当前块的末尾⑥,并启动一个新线程为当前块累加结果⑦。当迭代器指向当前块的末尾时,启动下一个块⑧。 70 | 71 | 启动所有线程后,⑨中的线程会处理最终块的结果。对于分配不均,因为知道最终块是哪一个,那么这个块中有多少个元素就无所谓了。 72 | 73 | 当累加最终块的结果后,可以等待`std::for_each`⑩创建线程的完成(如同在清单2.7中做的那样),之后使用`std::accumulate`将所有结果进行累加⑪。 74 | 75 | 结束这个例子之前,需要明确:T类型的加法运算不满足结合律(比如,对于float型或double型,在进行加法操作时,系统很可能会做截断操作),因为对范围中元素的分组,会导致parallel_accumulate得到的结果可能与`std::accumulate`得到的结果不同。同样的,这里对迭代器的要求更加严格:必须都是向前迭代器,而`std::accumulate`可以在只传入迭代器的情况下工作。对于创建出results容器,需要保证T有默认构造函数。对于算法并行,通常都要这样的修改;不过,需要根据算法本身的特性,选择不同的并行方式。算法并行会在第8章有更加深入的讨论。需要注意的:因为不能直接从一个线程中返回一个值,所以需要传递results容器的引用到线程中去。另一个办法,通过地址来获取线程执行的结果;第4章中,我们将使用*期望*(futures)完成这种方案。 76 | 77 | 当线程运行时,所有必要的信息都需要传入到线程中去,包括存储计算结果的位置。不过,并非总需如此:有时候这是识别线程的可行方案,可以传递一个标识数,例如清单2.7中的i。不过,当需要标识的函数在调用栈的深层,同时其他线程也可调用该函数,那么标识数就会变的捉襟见肘。好消息是在设计C++的线程库时,就有预见了这种情况,在之后的实现中就给每个线程附加了唯一标识符。 -------------------------------------------------------------------------------- /content/chapter2/2.5-chinese.md: -------------------------------------------------------------------------------- 1 | # 2.5 标识线程 2 | 3 | 线程标识类型是`std::thread::id`,可以通过两种方式进行检索。第一种,可以通过调用`std::thread`对象的成员函数`get_id()`来直接获取。如果`std::thread`对象没有与任何执行线程相关联,`get_id()`将返回`std::thread::type`默认构造值,这个值表示“无线程”。第二种,当前线程中调用`std::this_thread::get_id()`(这个函数定义在``头文件中)也可以获得线程标识。 4 | 5 | `std::thread::id`对象可以自由的拷贝和对比,因为标识符就可以复用。如果两个对象的`std::thread::id`相等,那它们就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。 6 | 7 | 线程库不会限制你去检查线程标识是否一样,`std::thread::id`类型对象提供相当丰富的对比操作;比如,提供为不同的值进行排序。这意味着允许程序员将其当做为容器的键值,做排序,或做其他方式的比较。按默认顺序比较不同值的`std::thread::id`,所以这个行为可预见的:当`a`容器,所以`std::thread::id`也可以作为无序容器的键值。 8 | 9 | `std::thread::id`实例常用作检测线程是否需要进行一些操作,比如:当用线程来分割一项工作(如清单2.8),主线程可能要做一些与其他线程不同的工作。这种情况下,启动其他线程前,它可以将自己的线程ID通过`std::this_thread::get_id()`得到,并进行存储。就是算法核心部分(所有线程都一样的),每个线程都要检查一下,其拥有的线程ID是否与初始线程的ID相同。 10 | 11 | ``` 12 | std::thread::id master_thread; 13 | void some_core_part_of_algorithm() 14 | { 15 | if(std::this_thread::get_id()==master_thread) 16 | { 17 | do_master_thread_work(); 18 | } 19 | do_common_work(); 20 | } 21 | ``` 22 | 23 | 另外,当前线程的`std::thread::id`将存储到一个数据结构中。之后在这个结构体中对当前线程的ID与存储的线程ID做对比,来决定操作是被“允许”,还是“需要”(permitted/required)。 24 | 25 | 同样,作为线程和本地存储不适配的替代方案,线程ID在容器中可作为键值。例如,容器可以存储其掌控下每个线程的信息,或在多个线程中互传信息。 26 | 27 | `std::thread::id`可以作为一个线程的通用标识符,当标识符只与语义相关(比如,数组的索引)时,就需要这个方案了。也可以使用输出流(`std::cout`)来记录一个`std::thread::id`对象的值。 28 | 29 | ``` 30 | std::cout< 6 | - 使用互斥量保护数据
7 | - 数据保护的替代方案
8 | 9 | 上一章中,我们已经对线程管理有所了解了,现在让我们来看一下“共享数据的那些事”。 10 | 11 | 想象一下,你和你的朋友合租一个公寓,公寓中只有一个厨房和一个卫生间。当你的朋友在卫生间时,你就会不能使用了(除非你们特别好,好到可以在同时使用一个房间)。这个问题也会出现在厨房,假如:厨房里有一个组合式烤箱,当在烤香肠的时候,也在做蛋糕,就可能得到我们不想要的食物(香肠味的蛋糕)。此外,在公共空间将一件事做到一半时,发现某些需要的东西被别人借走,或是当离开的一段时间内有些东西被变动了地方,这都会令我们不爽。 12 | 13 | 同样的问题,也困扰着线程。当线程在访问共享数据的时候,必须定一些规矩,用来限定线程可访问的数据位。还有,一个线程更新了共享数据,需要对其他线程进行通知。从易用性的角度,同一进程中的多个线程进行数据共享,有利有弊。错误的共享数据使用是产生并发bug的一个主要原因,并且后果要比香肠味的蛋糕更加严重。 14 | 15 | 本章就以在C++中进行安全的数据共享为主题。避免上述及其他潜在问题的发生的同时,将共享数据的优势发挥到最大。 -------------------------------------------------------------------------------- /content/chapter3/3.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 3.1 共享数据带来的问题 2 | 3 | 当涉及到共享数据时,问题很可能是因为共享数据修改所导致。如果共享数据是只读的,那么只读操作不会影响到数据,更不会涉及对数据的修改,所以所有线程都会获得同样的数据。但是,当一个或多个线程要修改共享数据时,就会产生很多麻烦。这种情况下,就必须小心谨慎,才能确保一切所有线程都工作正常。 4 | 5 | *不变量*(invariants)的概念对程序员们编写的程序会有一定的帮助——对于特殊结构体的描述;比如,“变量包含列表中的项数”。不变量通常会在一次更新中被破坏,特别是比较复杂的数据结构,或者一次更新就要改动很大的数据结构。 6 | 7 | 双链表中每个节点都有一个指针指向列表中下一个节点,还有一个指针指向前一个节点。其中不变量就是节点A中指向“下一个”节点B的指针,还有前向指针。为了从列表中删除一个节点,其两边节点的指针都需要更新。当其中一边更新完成时,不变量就被破坏了,直到另一边也完成更新;在两边都完成更新后,不变量就又稳定了。 8 | 9 | 从一个列表中删除一个节点的步骤如下(如图3.1) 10 | 11 | 1. 找到要删除的节点N
12 | 2. 更新前一个节点指向N的指针,让这个指针指向N的下一个节点
13 | 3. 更新后一个节点指向N的指针,让这个指正指向N的前一个节点
14 | 4. 删除节点N
15 | 16 | ![](../../images/chapter3/3-1.png) 17 | 18 | 图3.1 从一个双链表中删除一个节点 19 | 20 | 图中b和c在相同的方向上指向和原来已经不一致了,这就破坏了不变量。 21 | 22 | 线程间潜在问题就是修改共享数据,致使不变量遭到破坏。当不做些事来确保在这个过程中不会有其他线程进行访问的话,可能就有线程访问到刚刚删除一边的节点;这样的话,线程就读取到要删除节点的数据(因为只有一边的连接被修改,如图3.1(b)),所以不变量就被破坏。破坏不变量的后果是多样,当其他线程按从左往右的顺序来访问列表时,它将跳过被删除的节点。在一方面,如有第二个线程尝试删除图中右边的节点,那么可能会让数据结构产生永久性的损坏,使程序崩溃。无论结果如何,都是并行代码常见错误:条件竞争。 23 | 24 | ## 3.1.1 条件竞争 25 | 26 | 假设你去电影院买电影票。如果去的是一家大电影院,有很多收银台,很多人就可以在同一时间买电影票。当另一个收银台也在卖你想看的这场电影的电影票,那么你的座位选择范围就取决于在之前已预定的座位。当只有少量的座位剩下,这就意味着,这可能是一场抢票比赛,看谁能抢到最后一张票。这就是一个条件竞争的例子:你的座位(或者你的电影票)都取决于两种购买方式的相对顺序。 27 | 28 | 并发中竞争条件的形成,取决于一个以上线程的相对执行顺序,每个线程都抢着完成自己的任务。大多数情况下,即使改变执行顺序,也是良性竞争,其结果可以接受。例如,有两个线程同时向一个处理队列中添加任务,因为系统提供的不变量保持不变,所以谁先谁后都不会有什么影响。当不变量遭到破坏时,才会产生条件竞争,比如双向链表的例子。并发中对数据的条件竞争通常表示为恶性条件竞争,我们对不产生问题的良性条件竞争不感兴趣。`C++`标准中也定义了数据竞争这个术语,一种特殊的条件竞争:并发的去修改一个独立对象(参见5.1.2节),数据竞争是(可怕的)未定义行为的起因。 29 | 30 | 恶性条件竞争通常发生于完成对多于一个的数据块的修改时,例如,对两个连接指针的修改(如图3.1)。因为操作要访问两个独立的数据块,独立的指令将会对数据块将进行修改,并且其中一个线程可能正在进行时,另一个线程就对数据块进行了访问。因为出现的概率太低,条件竞争很难查找,也很难复现。如CPU指令连续修改完成后,即使数据结构可以让其他并发线程访问,问题再次复现的几率也相当低。当系统负载增加时,随着执行数量的增加,执行序列的问题复现的概率也在增加,这样的问题只可能会出现在负载比较大的情况下。条件竞争通常是时间敏感的,所以程序以调试模式运行时,它们常会完全消失,因为调试模式会影响程序的执行时间(即使影响不多)。 31 | 32 | 当你以写多线程程序为生,条件竞争就会成为你的梦魇;编写软件时,我们会使用大量复杂的操作,用来避免恶性条件竞争。 33 | 34 | ## 3.1.2 避免恶性条件竞争 35 | 36 | 这里提供一些方法来解决恶性条件竞争,最简单的办法就是对数据结构采用某种保护机制,确保只有进行修改的线程才能看到不变量被破坏时的中间状态。从其他访问线程的角度来看,修改不是已经完成了,就是还没开始。`C++`标准库提供很多类似的机制,下面会逐一介绍。 37 | 38 | 另一个选择是对数据结构和不变量的设计进行修改,修改完的结构必须能完成一系列不可分割的变化,也就是保证每个不变量保持稳定的状态,这就是所谓的无锁编程。不过,这种方式很难得到正确的结果。如果到这个级别,无论是内存模型上的细微差异,还是线程访问数据的能力,都会让工作变的复杂。内存模型将在第5章讨论,无锁编程将在第7章讨论。 39 | 40 | 另一种处理条件竞争的方式是,使用事务的方式去处理数据结构的更新(这里的"处理"就如同对数据库进行更新一样)。所需的一些数据和读取都存储在事务日志中,然后将之前的操作合为一步,再进行提交。当数据结构被另一个线程修改后,或处理已经重启的情况下,提交就会无法进行,这称作为“软件事务内存”。理论研究中,这是一个很热门的研究领域。这个概念将不会在本书中再进行介绍,因为在`C++`中没有对STM进行直接支持。但是,基本思想会在后面提及。 41 | 42 | 保护共享数据结构的最基本的方式,是使用C++标准库提供的互斥量。 -------------------------------------------------------------------------------- /content/chapter3/3.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 3.3 保护共享数据的替代设施 2 | 3 | 互斥量是最通用的机制,但其并非保护共享数据的唯一方式。这里有很多替代方式可以在特定情况下,提供更加合适的保护。 4 | 5 | 一个特别极端(但十分常见)的情况就是,共享数据在并发访问和初始化时(都需要保护),但是之后需要进行隐式同步。这可能是因为数据作为只读方式创建,所以没有同步问题;或者因为必要的保护作为对数据操作的一部分,所以隐式的执行。任何情况下,数据初始化后锁住一个互斥量,纯粹是为了保护其初始化过程(这是没有必要的),并且这会给性能带来不必要的冲击。出于以上的原因,`C++`标准提供了一种纯粹保护共享数据初始化过程的机制。 6 | 7 | ## 3.3.1 保护共享数据的初始化过程 8 | 9 | 假设你与一个共享源,构建代价很昂贵,可能它会打开一个数据库连接或分配出很多的内存。 10 | 11 | *延迟初始化*(Lazy initialization)在单线程代码很常见——每一个操作都需要先对源进行检查,为了了解数据是否被初始化,然后在其使用前决定,数据是否需要初始化: 12 | 13 | ``` 14 | std::shared_ptr resource_ptr; 15 | void foo() 16 | { 17 | if(!resource_ptr) 18 | { 19 | resource_ptr.reset(new some_resource); // 1 20 | } 21 | resource_ptr->do_something(); 22 | } 23 | ``` 24 | 25 | 当共享数据对于并发访问是安全的,①是转为多线程代码时,需要保护的,但是下面天真的转换会使得线程资源产生不必要的序列化。这是因为每个线程必须等待互斥量,为了确定数据源已经初始化了。 26 | 27 | 清单 3.11 使用一个互斥量的延迟初始化(线程安全)过程 28 | 29 | ``` 30 | std::shared_ptr resource_ptr; 31 | std::mutex resource_mutex; 32 | 33 | void foo() 34 | { 35 | std::unique_lock lk(resource_mutex); // 所有线程在此序列化 36 | if(!resource_ptr) 37 | { 38 | resource_ptr.reset(new some_resource); // 只有初始化过程需要保护 39 | } 40 | lk.unlock(); 41 | resource_ptr->do_something(); 42 | } 43 | ``` 44 | 45 | 这段代码相当常见了,也足够表现出没必要的线程化问题,很多人能想出更好的一些的办法来做这件事,包括声名狼藉的双重检查锁模式: 46 | 47 | ``` 48 | void undefined_behaviour_with_double_checked_locking() 49 | { 50 | if(!resource_ptr) // 1 51 | { 52 | std::lock_guard lk(resource_mutex); 53 | if(!resource_ptr) // 2 54 | { 55 | resource_ptr.reset(new some_resource); // 3 56 | } 57 | } 58 | resource_ptr->do_something(); // 4 59 | } 60 | ``` 61 | 62 | 指针第一次读取数据不需要获取锁①,并且只有在指针为NULL时才需要获取锁。然后,当获取锁之后,指针会被再次检查一遍② (这就是双重检查的部分),避免另一的线程在第一次检查后再做初始化,并且让当前线程获取锁。 63 | 64 | 这个模式为什么声名狼藉呢?因为这里有潜在的条件竞争,未被锁保护的读取操作①没有与其他线程里被锁保护的写入操作③进行同步。因此就会产生条件竞争,这个条件竞争不仅覆盖指针本身,还会影响到其指向的对象;即使一个线程知道另一个线程完成对指针进行写入,它可能没有看到新创建的some_resource实例,然后调用do_something()④后,得到不正确的结果。这个例子是在一种典型的条件竞争——数据竞争,`C++`标准中这就会被指定为“未定义行为”。这种竞争肯定是可以避免的。可以阅读第5章,那里有更多对内存模型的讨论,包括数据竞争的构成。 65 | 66 | C++标准委员会也认为条件竞争的处理很重要,所以C++标准库提供了`std::once_flag`和`std::call_once`来处理这种情况。比起锁住互斥量,并显式的检查指针,每个线程只需要使用`std::call_once`,在`std::call_once`的结束时,就能安全的知道指针已经被其他的线程初始化了。使用`std::call_once`比显式使用互斥量消耗的资源更少,特别是当初始化完成后。下面的例子展示了与清单3.11中的同样的操作,这里使用了`std::call_once`。在这种情况下,初始化通过调用函数完成,同样这样操作使用类中的函数操作符来实现同样很简单。如同大多数在标准库中的函数一样,或作为函数被调用,或作为参数被传递,`std::call_once`可以和任何函数或可调用对象一起使用。 67 | 68 | ``` 69 | std::shared_ptr resource_ptr; 70 | std::once_flag resource_flag; // 1 71 | 72 | void init_resource() 73 | { 74 | resource_ptr.reset(new some_resource); 75 | } 76 | 77 | void foo() 78 | { 79 | std::call_once(resource_flag,init_resource); // 可以完整的进行一次初始化 80 | resource_ptr->do_something(); 81 | } 82 | ``` 83 | 84 | 在这个例子中,`std::once_flag`①和初始化好的数据都是命名空间区域的对象,但是`std::call_once()`可仅作为延迟初始化的类型成员,如同下面的例子一样: 85 | 86 | 清单3.12 使用`std::call_once`作为类成员的延迟初始化(线程安全) 87 | 88 | ``` 89 | class X 90 | { 91 | private: 92 | connection_info connection_details; 93 | connection_handle connection; 94 | std::once_flag connection_init_flag; 95 | 96 | void open_connection() 97 | { 98 | connection=connection_manager.open(connection_details); 99 | } 100 | public: 101 | X(connection_info const& connection_details_): 102 | connection_details(connection_details_) 103 | {} 104 | void send_data(data_packet const& data) // 1 105 | { 106 | std::call_once(connection_init_flag,&X::open_connection,this); // 2 107 | connection.send_data(data); 108 | } 109 | data_packet receive_data() // 3 110 | { 111 | std::call_once(connection_init_flag,&X::open_connection,this); // 2 112 | return connection.receive_data(); 113 | } 114 | }; 115 | ``` 116 | 117 | 例子中第一个调用send_data()①或receive_data()③的线程完成初始化过程。使用成员函数open_connection()去初始化数据,也需要将this指针传进去。和其在在标准库中的函数一样,其接受可调用对象,比如`std::thread`的构造函数和`std::bind()`,通过向`std::call_once()`②传递一个额外的参数来完成这个操作。 118 | 119 | 值得注意的是,`std::mutex`和`std::one_flag`的实例就不能拷贝和移动,所以当你使用它们作为类成员函数,如果你需要用到他们,你就得显示定义这些特殊的成员函数。 120 | 121 | 还有一种情形的初始化过程中潜存着条件竞争:其中一个局部变量被声明为static类型。这种变量的在声明后就已经完成初始化;对于多线程调用的函数,这就意味着这里有条件竞争——抢着去定义这个变量。在很多在前C++11编译器(译者:不支持C++11标准的编译器),在实践过程中,这样的条件竞争是确实存在的,因为在多线程中,每个线程都认为他们是第一个初始化这个变量线程;或一个线程对变量进行初始化,而另外一个线程要使用这个变量时,初始化过程还没完成。在C++11标准中,这些问题都被解决了:初始化及定义完全在一个线程中发生,并且没有其他线程可在初始化完成前对其进行处理,条件竞争终止于初始化阶段,这样比在之后再去处理好的多。在只需要一个全局实例情况下,这里提供一个`std::call_once`的替代方案 122 | 123 | ``` 124 | class my_class; 125 | my_class& get_my_class_instance() 126 | { 127 | static my_class instance; // 线程安全的初始化过程 128 | return instance; 129 | } 130 | ``` 131 | 132 | 多线程可以安全的调用get_my_class_instance()①函数,不用为数据竞争而担心。 133 | 134 | 对于很少有更新的数据结构来说,只在初始化时保护数据。在大多数情况下,这种数据结构是只读的,并且多线程对其并发的读取也是很愉快的,不过一旦数据结构需要更新,就会产生竞争。 135 | 136 | ## 3.3.2 保护很少更新的数据结构 137 | 138 | 试想,为了将域名解析为其相关IP地址,我们在缓存中的存放了一张DNS入口表。通常,给定DNS数目在很长的一段时间内保持不变。虽然,在用户访问不同网站时,新的入口可能会被添加到表中,但是这些数据可能在其生命周期内保持不变。所以定期检查缓存中入口的有效性,就变的十分重要了;但是,这也需要一次更新,也许这次更新只是对一些细节做了改动。 139 | 140 | 虽然更新频度很低,但更新也有可能发生,并且当这个可缓存被多个线程访问,这个缓存就需要处于更新状态时得到保护,这也为了确保每个线程读到都是有效数据。 141 | 142 | 没有使用专用数据结构时,这种方式是符合预期,并且为并发更新和读取特别设计的(更多的例子在第6和第7章中介绍)。这样的更新要求线程独占数据结构的访问权,直到其完成更新操作。当更新完成,数据结构对于并发多线程访问又会是安全的。使用`std::mutex`来保护数据结构,显的有些反应过度(因为在没有发生修改时,它将削减并发读取数据的可能性)。这里需要另一种不同的互斥量,这种互斥量常被称为“读者-作者锁”,因为其允许两种不同的使用方式:一个“作者”线程独占访问和共享访问,让多个“读者”线程并发访问。 143 | 144 | 虽然这样互斥量的标准提案已经交给标准委员会,但是`C++`标准库依旧不会提供这样的互斥量[3]。因为建议没有被采纳,这个例子在本节中使用的是Boost库提供的实现(Boost采纳了这个建议)。你将在第8章中看到,这种锁的也不能包治百病,其性能依赖于参与其中的处理器数量,同样也与读者和作者线程的负载有关。为了确保增加复杂度后还能获得性能收益,目标系统上的代码性能就很重要。 145 | 146 | 比起使用`std::mutex`实例进行同步,不如使用`boost::shared_mutex`来做同步。对于更新操作,可以使用`std::lock_guard`和`std::unique_lock`上锁。作为`std::mutex`的替代方案,与`std::mutex`所做的一样,这就能保证更新线程的独占访问。因为其他线程不需要去修改数据结构,所以其可以使用`boost::shared_lock`获取访问权。这与使用`std::unique_lock`一样,除非多线程要在同时获取同一个`boost::shared_mutex`上有共享锁。唯一的限制:当任一线程拥有一个共享锁时,这个线程就会尝试获取一个独占锁,直到其他线程放弃他们的锁;同样的,当任一线程拥有一个独占锁时,其他线程就无法获得共享锁或独占锁,直到第一个线程放弃其拥有的锁。 147 | 148 | 如同之前描述的那样,下面的代码清单展示了一个简单的DNS缓存,使用`std::map`持有缓存数据,使用`boost::shared_mutex`进行保护。 149 | 150 | 清单3.13 使用`boost::shared_mutex`对数据结构进行保护 151 | 152 | ``` 153 | #include 154 | #include 155 | #include 156 | #include 157 | 158 | class dns_entry; 159 | 160 | class dns_cache 161 | { 162 | std::map entries; 163 | mutable boost::shared_mutex entry_mutex; 164 | public: 165 | dns_entry find_entry(std::string const& domain) const 166 | { 167 | boost::shared_lock lk(entry_mutex); // 1 168 | std::map::const_iterator const it= 169 | entries.find(domain); 170 | return (it==entries.end())?dns_entry():it->second; 171 | } 172 | void update_or_add_entry(std::string const& domain, 173 | dns_entry const& dns_details) 174 | { 175 | std::lock_guard lk(entry_mutex); // 2 176 | entries[domain]=dns_details; 177 | } 178 | }; 179 | ``` 180 | 181 | 清单3.13中,find_entry()使用`boost::shared_lock<>`来保护共享和只读权限①;这就使得多线程可以同时调用find_entry(),且不会出错。另一方面,update_or_add_entry()使用`std::lock_guard<>`实例,当表格需要更新时②,为其提供独占访问权限;update_or_add_entry()函数调用时,独占锁会阻止其他线程对数据结构进行修改,并且阻止线程调用find_entry()。 182 | 183 | ## 3.3.3 嵌套锁 184 | 185 | 当一个线程已经获取一个`std::mutex`时(已经上锁),并对其再次上锁,这个操作就是错误的,并且继续尝试这样做的话,就会产生未定义行为。然而,在某些情况下,一个线程尝试获取同一个互斥量多次,而没有对其进行一次释放是可以的。之所以可以,是因为`C++`标准库提供了`std::recursive_mutex`类。其功能与`std::mutex`类似,除了你可以从同一线程的单个实例上获取多个锁。互斥量锁住其他线程前,你必须释放你拥有的所有锁,所以当你调用lock()三次时,你也必须调用unlock()三次。正确使用`std::lock_guard`和`std::unique_lock`可以帮你处理这些问题。 186 | 187 | 大多数情况下,当你需要嵌套锁时,就要对你的设计进行改动。嵌套锁一般用在可并发访问的类上,所以其拥互斥量保护其成员数据。每个公共成员函数都会对互斥量上锁,然后完成对应的功能,之后再解锁互斥量。不过,有时成员函数会调用另一个成员函数,这种情况下,第二个成员函数也会试图锁住互斥量,这就会导致未定义行为的发生。“变通的”解决方案会将互斥量转为嵌套锁,第二个成员函数就能成功的进行上锁,并且函数能继续执行。 188 | 189 | 但是,这样的使用方式是不推荐的,因为其过于草率,并且不合理。特别是,当锁被持有时,对应类的不变量通常正在被修改。这意味着,当不变量正在改变的时候,第二个成员函数还需要继续执行。一个比较好的方式是,从中提取出一个函数作为类的私有成员,并且让其他成员函数都对其进行调用,这个私有成员函数不会对互斥量进行上锁(在调用前必须获得锁)。然后,你仔细考虑一下,在这种情况调用新函数时,数据的状态。 190 | 191 | ------- 192 | 193 | [3] Howard E. Hinnant, “Multithreading API for C++0X—A Layered Approach,” C++ Standards Committee Paper N2094, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2094.html. 194 | -------------------------------------------------------------------------------- /content/chapter3/3.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 3.4 本章总结 2 | 3 | 本章讨论了当两个线程间的共享数据发生恶性条件竞争会带来多么严重的灾难,还讨论了如何使用`std::mutex`,和如何避免这些问题。如你所见,互斥量并不是灵丹妙药,其还有自己的问题(比如:死锁),虽然C++标准库提供了一类工具来避免这些(例如:`std::lock()`)。你还见识了一些用于避免死锁的先进技术,之后了解了锁所有权的转移,以及一些围绕如何选取适当粒度锁产生的问题。最后,讨论了在具体情况下,数据保护的替代方案,例如:`std::call_once()`和`boost::shared_mutex`。 4 | 5 | 还有一个方面没有涉及到,那就是等待其他线程作为输入的情况。我们的线程安全栈,仅是在栈为空时,抛出一个异常,所以当一个线程要等待其他线程向栈压入一个值时(这是一个线程安全栈的主要用途之一),它不得不多次尝试去弹出一个值,当捕获抛出的异常时,再次进行尝试。这种消耗资源的检查,没有任何意义。并且,不断的检查会影响系统中其他线程的运行,这反而会妨碍程序的进展。我们需要一些方法让一个线程等待其他线程完成任务,但在等待过程中不占用CPU。第4章中,会去建立一些工具,用于保护共享数据,还会介绍一些线程同步操作的机制;第6章中,如何构建更大型的可复用的数据类型。 -------------------------------------------------------------------------------- /content/chapter4/4.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第4章 同步并发操作 2 | 3 | **本章主要内容** 4 | 5 | - 等待事件
6 | - 带有期望的等待一次性事件
7 | - 在限定时间内等待
8 | - 使用同步操作简化代码
9 | 10 | 在上一章中,我们看到各种在线程间保护共享数据的方法。当你不仅想要保护数据,还想对单独的线程进行同步。例如,在第一个线程完成前,可能需要等待另一个线程执行完成。通常情况下,线程会等待一个特定事件的发生,或者等待某一条件达成(为true)。这可能需要定期检查“任务完成”标识,或将类似的东西放到共享数据中,但这与理想情况还是差很多。像这种情况就需要在线程中进行同步,`C++`标准库提供了一些工具可用于同步操作,形式上表现为*条件变量*(condition variables)和*期望*(futures)。 11 | 12 | 在本章,将讨论如何使用条件变量等待事件,以及介绍期望,和如何使用它简化同步操作。 -------------------------------------------------------------------------------- /content/chapter4/4.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 4.1 等待一个事件或其他条件 2 | 3 | 假设你在旅游,而且正在一辆在夜间运行的火车上。在夜间,如何在正确的站点下车呢?一种方法是整晚都要醒着,然后注意到了哪一站。这样,你就不会错过你要到达的站点,但是这样会让你感到很疲倦。另外,你可以看一下时间表,估计一下火车到达目的地的时间,然后在一个稍早的时间点上设置闹铃,然后你就可以安心的睡会了。这个方法听起来也很不错,也没有错过你要下车的站点,但是当火车晚点的时候,你就要被过早的叫醒了。当然,闹钟的电池也可能会没电了,并导致你睡过站。理想的方式是,无论是早或晚,只要当火车到站的时候,有人或其他东西能把你唤醒,就好了。 4 | 5 | 这和线程有什么关系呢?好吧,让我们来联系一下。当一个线程等待另一个线程完成任务时,它会有很多选择。第一,它可以持续的检查共享数据标志(用于做保护工作的互斥量),直到另一线程完成工作时对这个标志进行重设。不过,就是一种浪费:线程消耗宝贵的执行时间持续的检查对应标志,并且当互斥量被等待线程上锁后,其他线程就没有办法获取锁,这样线程就会持续等待。因为以上方式对等待线程限制资源,并且在完成时阻碍对标识的设置。这种情况类似与,保持清醒状态和列车驾驶员聊了一晚上:驾驶员不得不缓慢驾驶,因为你分散了他的注意力,所以火车需要更长的时间,才能到站。同样的,等待的线程会等待更长的时间,这些线程也在消耗着系统资源。 6 | 7 | 第二个选择是在等待线程在检查间隙,使用`std::this_thread::sleep_for()`进行周期性的间歇(详见4.3节): 8 | 9 | ``` 10 | bool flag; 11 | std::mutex m; 12 | 13 | void wait_for_flag() 14 | { 15 | std::unique_lock lk(m); 16 | while(!flag) 17 | { 18 | lk.unlock(); // 1 解锁互斥量 19 | std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 2 休眠100ms 20 | lk.lock(); // 3 再锁互斥量 21 | } 22 | } 23 | ``` 24 | 25 | 这个循环中,在休眠前②,函数对互斥量进行解锁①,并且在休眠结束后再对互斥量进行上锁,所以另外的线程就有机会获取锁并设置标识。 26 | 27 | 这个实现就进步很多,因为当线程休眠时,线程没有浪费执行时间,但是很难确定正确的休眠时间。太短的休眠和没有休眠一样,都会浪费执行时间;太长的休眠时间,可能会让任务等待线程醒来。休眠时间过长是很少见的情况,因为这会直接影响到程序的行为,当在高节奏游戏中,它意味着丢帧,或在一个实时应用中超越了一个时间片。 28 | 29 | 第三个选择(也是优先的选择)是,使用C++标准库提供的工具去等待事件的发生。通过另一线程触发等待事件的机制是最基本的唤醒方式(例如:流水线上存在额外的任务时),这种机制就称为“条件变量”。从概念上来说,一个条件变量会与多个事件或其他条件相关,并且一个或多个线程会等待条件的达成。当某些线程被终止时,为了唤醒等待线程(允许等待线程继续执行)终止的线程将会向等待着的线程广播“条件达成”的信息。 30 | 31 | ## 4.1.1 等待条件达成 32 | 33 | C++标准库对条件变量有两套实现:`std::condition_variable`和`std::condition_variable_any`。这两个实现都包含在``头文件的声明中。两者都需要与一个互斥量一起才能工作(互斥量是为了同步);前者仅限于与`std::mutex`一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了*_any*的后缀。因为` std::condition_variable_any`更加通用,这就可能从体积、性能,以及系统资源的使用方面产生额外的开销,所以`std::condition_variable`一般作为首选的类型,当对灵活性有硬性要求时,我们才会去考虑`std::condition_variable_any`。 34 | 35 | 所以,如何使用`std::condition_variable`去处理之前提到的情况——当有数据需要处理时,如何唤醒休眠中的线程对其进行处理?以下清单展示了一种使用条件变量做唤醒的方式。 36 | 37 | 清单4.1 使用`std::condition_variable`处理数据等待 38 | 39 | ``` 40 | std::mutex mut; 41 | std::queue data_queue; // 1 42 | std::condition_variable data_cond; 43 | 44 | void data_preparation_thread() 45 | { 46 | while(more_data_to_prepare()) 47 | { 48 | data_chunk const data=prepare_data(); 49 | std::lock_guard lk(mut); 50 | data_queue.push(data); // 2 51 | data_cond.notify_one(); // 3 52 | } 53 | } 54 | 55 | void data_processing_thread() 56 | { 57 | while(true) 58 | { 59 | std::unique_lock lk(mut); // 4 60 | data_cond.wait( 61 | lk,[]{return !data_queue.empty();}); // 5 62 | data_chunk data=data_queue.front(); 63 | data_queue.pop(); 64 | lk.unlock(); // 6 65 | process(data); 66 | if(is_last_chunk(data)) 67 | break; 68 | } 69 | } 70 | ``` 71 | 72 | 首先,你拥有一个用来在两个线程之间传递数据的队列①。当数据准备好时,使用`std::lock_guard`对队列上锁,将准备好的数据压入队列中②,之后线程会对队列中的数据上锁。然后调用`std::condition_variable`的notify_one()成员函数,对等待的线程(如果有等待线程)进行通知③。 73 | 74 | 在另外一侧,你有一个正在处理数据的线程,这个线程首先对互斥量上锁,但在这里`std::unique_lock`要比`std::lock_guard`④更加合适——且听我细细道来。线程之后会调用`std::condition_variable`的成员函数wait(),传递一个锁和一个lambda函数表达式(作为等待的条件⑤)。Lambda函数是`C++11`添加的新特性,它可以让一个匿名函数作为其他表达式的一部分,并且非常合适作为标准函数的谓词,例如wait()函数。在这个例子中,简单的lambda函数`[]{return !data_queue.empty();}`会去检查data_queue是否不为空,当data_queue不为空——那就意味着队列中已经准备好数据了。附录A的A.5节有Lambda函数更多的信息。 75 | 76 | wait()会去检查这些条件(通过调用所提供的lambda函数),当条件满足(lambda函数返回true)时返回。如果条件不满足(lambda函数返回false),wait()函数将解锁互斥量,并且将这个线程(上段提到的处理数据的线程)置于阻塞或等待状态。当准备数据的线程调用notify_one()通知条件变量时,处理数据的线程从睡眠状态中苏醒,重新获取互斥锁,并且对条件再次检查,在条件满足的情况下,从wait()返回并继续持有锁。当条件不满足时,线程将对互斥量解锁,并且重新开始等待。这就是为什么用`std::unique_lock`而不使用`std::lock_guard`——等待中的线程必须在等待期间解锁互斥量,并在这这之后对互斥量再次上锁,而`std::lock_guard`没有这么灵活。如果互斥量在线程休眠期间保持锁住状态,准备数据的线程将无法锁住互斥量,也无法添加数据到队列中;同样的,等待线程也永远不会知道条件何时满足。 77 | 78 | 清单4.1使用了一个简单的lambda函数用于等待⑤,这个函数用于检查队列何时不为空,不过任意的函数和可调用对象都可以传入wait()。当你已经写好了一个函数去做检查条件(或许比清单中简单检查要复杂很多),那就可以直接将这个函数传入wait();不一定非要放在一个lambda表达式中。在调用wait()的过程中,一个条件变量可能会去检查给定条件若干次;然而,它总是在互斥量被锁定时这样做,当且仅当提供测试条件的函数返回true时,它就会立即返回。当等待线程重新获取互斥量并检查条件时,如果它并非直接响应另一个线程的通知,这就是所谓的*伪唤醒*(spurious wakeup)。因为任何伪唤醒的数量和频率都是不确定的,这里不建议使用一个有副作用的函数做条件检查。当你这样做了,就必须做好多次产生副作用的心理准备。 79 | 80 | 解锁`std::unique_lock`的灵活性,不仅适用于对wait()的调用;它还可以用于有待处理但还未处理的数据⑥。处理数据可能是一个耗时的操作,并且如你在第3章见到的,你就知道持有锁的时间过长是一个多么糟糕的主意。 81 | 82 | 使用队列在多个线程中转移数据(如清单4.1)是很常见的。做得好的话,同步操作可以限制在队列本身,同步问题和条件竞争出现的概率也会降低。鉴于这些好处,现在从清单4.1中提取出一个通用线程安全的队列。 83 | 84 | ## 4.1.2 使用条件变量构建线程安全队列 85 | 86 | 当你正在设计一个通用队列时,花一些时间想想有哪些操作需要添加到队列实现中去,就如之前在3.2.3节看到的线程安全的栈。可以看一下C++标准库提供的实现,找找灵感;`std::queue<>`容器的接口展示如下: 87 | 88 | 清单4.2 `std::queue`接口 89 | 90 | ``` 91 | template > 92 | class queue { 93 | public: 94 | explicit queue(const Container&); 95 | explicit queue(Container&& = Container()); 96 | template explicit queue(const Alloc&); 97 | template queue(const Container&, const Alloc&); 98 | template queue(Container&&, const Alloc&); 99 | template queue(queue&&, const Alloc&); 100 | 101 | void swap(queue& q); 102 | 103 | bool empty() const; 104 | size_type size() const; 105 | 106 | T& front(); 107 | const T& front() const; 108 | T& back(); 109 | const T& back() const; 110 | 111 | void push(const T& x); 112 | void push(T&& x); 113 | void pop(); 114 | template void emplace(Args&&... args); 115 | }; 116 | ``` 117 | 118 | 当你忽略构造、赋值以及交换操作时,你就剩下了三组操作:1. 对整个队列的状态进行查询(empty()和size());2.查询在队列中的各个元素(front()和back());3.修改队列的操作(push(), pop()和emplace())。这就和3.2.3中的栈一样了,因此你也会遇到在固有接口上的条件竞争。因此,你需要将front()和pop()合并成一个函数调用,就像之前在栈实现时合并top()和pop()一样。与清单4.1中的代码不同的是:当使用队列在多个线程中传递数据时,接收线程通常需要等待数据的压入。这里我们提供pop()函数的两个变种:try_pop()和wait_and_pop()。try_pop() ,尝试从队列中弹出数据,总会直接返回(当有失败时),即使没有值可检索;wait_and_pop(),将会等待有值可检索的时候才返回。当你使用之前栈的方式来实现你的队列,你实现的队列接口就可能会是下面这样: 119 | 120 | 清单4.3 线程安全队列的接口 121 | 122 | ``` 123 | #include // 为了使用std::shared_ptr 124 | 125 | template 126 | class threadsafe_queue 127 | { 128 | public: 129 | threadsafe_queue(); 130 | threadsafe_queue(const threadsafe_queue&); 131 | threadsafe_queue& operator=( 132 | const threadsafe_queue&) = delete; // 不允许简单的赋值 133 | 134 | void push(T new_value); 135 | 136 | bool try_pop(T& value); // 1 137 | std::shared_ptr try_pop(); // 2 138 | 139 | void wait_and_pop(T& value); 140 | std::shared_ptr wait_and_pop(); 141 | 142 | bool empty() const; 143 | }; 144 | ``` 145 | 146 | 就像之前对栈做的那样,在这里你将很多构造函数剪掉了,并且禁止了对队列的简单赋值。和之前一样,你也需要提供两个版本的try_pop()和wait_for_pop()。第一个重载的try_pop()①在引用变量中存储着检索值,所以它可以用来返回队列中值的状态;当检索到一个变量时,他将返回true,否则将返回false(详见A.2节)。第二个重载②就不能做这样了,因为它是用来直接返回检索值的。当没有值可检索时,这个函数可以返回NULL指针。 147 | 148 | 那么问题来了,如何将以上这些和清单4.1中的代码相关联呢?好吧,我们现在就来看看怎么去关联。你可以从之前的代码中提取push()和wait_and_pop(),如以下清单所示。 149 | 150 | 清单4.4 从清单4.1中提取push()和wait_and_pop() 151 | 152 | ``` 153 | #include 154 | #include 155 | #include 156 | 157 | template 158 | class threadsafe_queue 159 | { 160 | private: 161 | std::mutex mut; 162 | std::queue data_queue; 163 | std::condition_variable data_cond; 164 | public: 165 | void push(T new_value) 166 | { 167 | std::lock_guard lk(mut); 168 | data_queue.push(new_value); 169 | data_cond.notify_one(); 170 | } 171 | 172 | void wait_and_pop(T& value) 173 | { 174 | std::unique_lock lk(mut); 175 | data_cond.wait(lk,[this]{return !data_queue.empty();}); 176 | value=data_queue.front(); 177 | data_queue.pop(); 178 | } 179 | }; 180 | threadsafe_queue data_queue; // 1 181 | 182 | void data_preparation_thread() 183 | { 184 | while(more_data_to_prepare()) 185 | { 186 | data_chunk const data=prepare_data(); 187 | data_queue.push(data); // 2 188 | } 189 | } 190 | 191 | void data_processing_thread() 192 | { 193 | while(true) 194 | { 195 | data_chunk data; 196 | data_queue.wait_and_pop(data); // 3 197 | process(data); 198 | if(is_last_chunk(data)) 199 | break; 200 | } 201 | } 202 | ``` 203 | 204 | 线程队列的实例中包含有互斥量和条件变量,所以独立的变量就不需要了①,并且调用push()也不需要外部同步②。当然,wait_and_pop()还要兼顾条件变量的等待③。 205 | 206 | 另一个wait_and_pop()函数的重载写起来就很琐碎了,剩下的函数就像从清单3.5实现的栈中一个个的粘过来一样。最终的队列实现如下所示。 207 | 208 | 清单4.5 使用条件变量的线程安全队列(完整版) 209 | 210 | ``` 211 | #include 212 | #include 213 | #include 214 | #include 215 | 216 | template 217 | class threadsafe_queue 218 | { 219 | private: 220 | mutable std::mutex mut; // 1 互斥量必须是可变的 221 | std::queue data_queue; 222 | std::condition_variable data_cond; 223 | public: 224 | threadsafe_queue() 225 | {} 226 | threadsafe_queue(threadsafe_queue const& other) 227 | { 228 | std::lock_guard lk(other.mut); 229 | data_queue=other.data_queue; 230 | } 231 | 232 | void push(T new_value) 233 | { 234 | std::lock_guard lk(mut); 235 | data_queue.push(new_value); 236 | data_cond.notify_one(); 237 | } 238 | 239 | void wait_and_pop(T& value) 240 | { 241 | std::unique_lock lk(mut); 242 | data_cond.wait(lk,[this]{return !data_queue.empty();}); 243 | value=data_queue.front(); 244 | data_queue.pop(); 245 | } 246 | 247 | std::shared_ptr wait_and_pop() 248 | { 249 | std::unique_lock lk(mut); 250 | data_cond.wait(lk,[this]{return !data_queue.empty();}); 251 | std::shared_ptr res(std::make_shared(data_queue.front())); 252 | data_queue.pop(); 253 | return res; 254 | } 255 | 256 | bool try_pop(T& value) 257 | { 258 | std::lock_guard lk(mut); 259 | if(data_queue.empty()) 260 | return false; 261 | value=data_queue.front(); 262 | data_queue.pop(); 263 | return true; 264 | } 265 | 266 | std::shared_ptr try_pop() 267 | { 268 | std::lock_guard lk(mut); 269 | if(data_queue.empty()) 270 | return std::shared_ptr(); 271 | std::shared_ptr res(std::make_shared(data_queue.front())); 272 | data_queue.pop(); 273 | return res; 274 | } 275 | 276 | bool empty() const 277 | { 278 | std::lock_guard lk(mut); 279 | return data_queue.empty(); 280 | } 281 | }; 282 | ``` 283 | 284 | empty()是一个const成员函数,并且传入拷贝构造函数的other形参是一个const引用;因为其他线程可能有这个类型的非const引用对象,并调用变种成员函数,所以这里有必要对互斥量上锁。如果锁住互斥量是一个可变操作,那么这个互斥量对象就会标记为可变的①,之后他就可以在empty()和拷贝构造函数中上锁了。 285 | 286 | 条件变量在多个线程等待同一个事件时,也是很有用的。当线程用来分解工作负载,并且只有一个线程可以对通知做出反应,与清单4.1中使用的结构完全相同;运行多个数据实例——*处理线程*(processing thread)。当新的数据准备完成,调用notify_one()将会触发一个正在执行wait()的线程,去检查条件和wait()函数的返回状态(因为你仅是向data_queue添加一个数据项)。 这里不保证线程一定会被通知到,即使只有一个等待线程被通知时,所有处线程也有可能都在处理数据。 287 | 288 | 另一种可能是,很多线程等待同一事件,对于通知他们都需要做出回应。这会发生在共享数据正在初始化的时候,当处理线程可以使用同一数据时,就要等待数据被初始化(有不错的机制可用来应对;可见第3章,3.3.1节),或等待共享数据的更新,比如,*定期重新初始化*(periodic reinitialization)。在这些情况下,准备线程准备数据数据时,就会通过条件变量调用notify_all()成员函数,而非直接调用notify_one()函数。顾名思义,这就是全部线程在都去执行wait()(检查他们等待的条件是否满足)的原因。 289 | 290 | 当等待线程只等待一次,当条件为true时,它就不会再等待条件变量了,所以一个条件变量可能并非同步机制的最好选择。尤其是,条件在等待一组可用的数据块时。在这样的情况下,*期望*(future)就是一个适合的选择。 -------------------------------------------------------------------------------- /content/chapter4/4.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 4.3 限定等待时间 2 | 3 | 之前介绍过的所有阻塞调用,将会阻塞一段不确定的时间,将线程挂起直到等待的事件发生。在很多情况下,这样的方式很不错,但是在其他一些情况下,你就需要限制一下线程等待的时间了。这允许你发送一些类似“我还存活”的信息,无论是对交互式用户,或是其他进程,亦或当用户放弃等待,你可以按下“取消”键直接终止等待。 4 | 5 | 介绍两种可能是你希望指定的超时方式:一种是“时延”的超时方式,另一种是“绝对”超时方式。第一种方式,需要指定一段时间(例如,30毫秒);第二种方式,就是指定一个时间点(例如,协调世界时[UTC]17:30:15.045987023,2011年11月30日)。多数等待函数提供变量,对两种超时方式进行处理。处理持续时间的变量以“_for”作为后缀,处理绝对时间的变量以"_until"作为后缀。 6 | 7 | 所以,当`std::condition_variable`的两个成员函数wait_for()和wait_until()成员函数分别有两个负载,这两个负载都与wait()成员函数的负载相关——其中一个负载只是等待信号触发,或时间超期,亦或是一个虚假的唤醒,并且醒来时,会检查锁提供的谓词,并且只有在检查为true时才会返回(这时条件变量的条件达成),或直接而超时。 8 | 9 | 在我们观察使用超时函数的细节前,让我们来检查一下时间在C++中指定的方式,就从时钟开始吧! 10 | 11 | ### 4.3.1 时钟 12 | 13 | 对于C++标准库来说,时钟就是时间信息源。特别是,时钟是一个类,提供了四种不同的信息: 14 | 15 | * 现在时间 16 | 17 | * 时间类型 18 | 19 | * 时钟节拍 20 | 21 | * 通过时钟节拍的分布,判断时钟是否稳定 22 | 23 | 时钟的当前时间可以通过调用静态成员函数now()从时钟类中获取;例如,`std::chrono::system_clock::now()`是将返回系统时钟的当前时间。特定的时间点类型可以通过time_point的数据typedef成员来指定,所以some_clock::now()的类型就是some_clock::time_point。 24 | 25 | 时钟节拍被指定为1/x(x在不同硬件上有不同的值)秒,这是由时间周期所决定——一个时钟一秒有25个节拍,因此一个周期为`std::ratio<1, 25>`,当一个时钟的时钟节拍每2.5秒一次,周期就可以表示为`std::ratio<5, 2>`。当时钟节拍直到运行时都无法知晓,可以使用一个给定的应用程序运行多次,周期可以用执行的平均时间求出,其中最短的时间可能就是时钟节拍,或者是直接写在手册当中。这就不保证在给定应用中观察到的节拍周期与指定的时钟周期相匹配。 26 | 27 | 当时钟节拍均匀分布(无论是否与周期匹配),并且不可调整,这种时钟就称为稳定时钟。当is_steady静态数据成员为true时,表明这个时钟就是稳定的,否则,就是不稳定的。通常情况下,`std::chrono::system_clock`是不稳定的,因为时钟是可调的,即是这种是完全自动适应本地账户的调节。这种调节可能造成的是,首次调用now()返回的时间要早于上次调用now()所返回的时间,这就违反了节拍频率的均匀分布。稳定闹钟对于超时的计算很重要,所以C++标准库提供一个稳定时钟`std::chrono::steady_clock`。C++标准库提供的其他时钟可表示为`std::chrono::system_clock`(在上面已经提到过),它代表了系统时钟的“实际时间”,并且提供了函数可将时间点转化为time_t类型的值;`std::chrono::high_resolution_clock` 可能是标准库中提供的具有最小节拍周期(因此具有最高的精度[分辨率])的时钟。它实际上是typedef的另一种时钟,这些时钟和其他与时间相关的工具,都被定义在库头文件中。 28 | 29 | 我们马上来看一下时间点是如何表示的,但在这之前,我们先看一下持续时间是怎么表示的。 30 | 31 | ### 4.3.2 时延 32 | 33 | 时延是时间部分最简单的;`std::chrono::duration<>`函数模板能够对时延进行处理(线程库使用到的所有C++时间处理工具,都在`std::chrono`命名空间内)。第一个模板参数是一个类型表示(比如,int,long或double),第二个模板参数是制定部分,表示每一个单元所用秒数。例如,当几分钟的时间要存在short类型中时,可以写成`std::chrono::duration>`,因为60秒是才是1分钟,所以第二个参数写成`std::ratio<60, 1>`。另一方面,当需要将毫秒级计数存在double类型中时,可以写成`std::chrono::duration>`,因为1秒等于1000毫秒。 34 | 35 | 标准库在`std::chrono`命名空间内,为延时变量提供一系列预定义类型:nanoseconds[纳秒] , microseconds[微秒] , milliseconds[毫秒] , seconds[秒] , minutes[分]和hours[时]。比如,你要在一个合适的单元表示一段超过500年的时延,预定义类型可充分利用了大整型,来表示所要表示的时间类型。当然,这里也定义了一些国际单位制(SI, [法]le Système international d'unités)分数,可从`std::atto(10^(-18))`到`std::exa(10^(18))`(题外话:当你的平台支持128位整型);也可以指定自定义时延类型,例如,`std::duration`,就可以使用一个double类型的变量表示1/100。 36 | 37 | 当不要求截断值的情况下(时转换成秒是没问题,但是秒转换成时就不行)时延的转换是隐式的。显示转换可以由`std::chrono::duration_cast<>`来完成。 38 | 39 | ``` 40 | std::chrono::milliseconds ms(54802); 41 | std::chrono::seconds s= 42 | std::chrono::duration_cast(ms); 43 | ``` 44 | 45 | 这里的结果就是截断的,而不是进行了舍入,所以s最后的值将为54。 46 | 47 | 延迟支持计算,所以你能够对两个时延变量进行加减,或者是对一个时延变量乘除一个常数(模板的第一个参数)来获得一个新延迟变量。例如,5*seconds(1)与seconds(5)或minutes(1)-seconds(55)一样。在时延中可以通过count()成员函数获得单位时间的数量。例如,`std::chrono::milliseconds(1234).count()`就是1234。 48 | 49 | 基于时延的等待可由`std::chrono::duration<>`来完成。例如,你等待一个“期望”状态变为就绪已经35毫秒: 50 | 51 | ``` 52 | std::future f=std::async(some_task); 53 | if(f.wait_for(std::chrono::milliseconds(35))==std::future_status::ready) 54 | do_something_with(f.get()); 55 | ``` 56 | 57 | 等待函数会返回一个状态值,来表示等待是超时,还是继续等待。在这种情况下,你可以等待一个“期望”,所以当函数等待超时时,会返回`std::future_status::timeout`;当“期望”状态改变,函数会返回`std::future_status::ready`;当“期望”的任务延迟了,函数会返回`std::future_status::deferred`。基于时延的等待是使用内部库提供的稳定时钟,来进行计时的;所以,即使系统时钟在等待时被调整(向前或向后),35毫秒的时延在这里意味着,的确耗时35毫秒。当然,难以预料的系统调度和不同操作系统的时钟精度都意味着:在线程中,从调用到返回的实际时间可能要比35毫秒长。 58 | 59 | 时延中没有特别好的办法来处理以上情况,所以我们暂且停下对时延的讨论。现在,我们就要来看看“时间点”是怎么样工作的。 60 | 61 | ### 4.3.3 时间点 62 | 63 | 时钟的时间点可以用`std::chrono::time_point<>`的类型模板实例来表示,实例的第一个参数用来指定所要使用的时钟,第二个函数参数用来表示时间的计量单位(特化的`std::chrono::duration<>`)。一个时间点的值就是时间的长度(在指定时间的倍数内),例如,指定“unix时间戳”(*epoch*)为一个时间点。时间戳是时钟的一个基本属性,但是不可以直接查询,或在C++标准中已经指定。通常,unix时间戳表示1970年1月1日 00:00,即计算机启动应用程序时。时钟可能共享一个时间戳,或具有独立的时间戳。当两个时钟共享一个时间戳时,其中一个time_point类型可以与另一个时钟类型中的time_point相关联。这里,虽然你无法知道unix时间戳是什么,但是你可以通过对指定time_point类型使用time_since_epoch()来获取时间戳。这个成员函数会返回一个时延值,这个时延值是指定时间点到时钟的unix时间戳锁用时。 64 | 65 | 例如,你可能指定了一个时间点`std::chrono::time_point`。这就与系统时钟有关,且实际中的一分钟与系统时钟精度应该不相同(通常差几秒)。 66 | 67 | 你可以通过`std::chrono::time_point<>`实例来加/减时延,来获得一个新的时间点,所以`std::chrono::hight_resolution_clock::now() + std::chrono::nanoseconds(500)`将得到500纳秒后的时间。当你知道一块代码的最大时延时,这对于计算绝对时间的超时是一个好消息,当等待时间内,等待函数进行多次调用;或,非等待函数且占用了等待函数时延中的时间。 68 | 69 | 你也可以减去一个时间点(二者需要共享同一个时钟)。结果是两个时间点的时间差。这对于代码块的计时是很有用的,例如: 70 | 71 | ``` 72 | auto start=std::chrono::high_resolution_clock::now(); 73 | do_something(); 74 | auto stop=std::chrono::high_resolution_clock::now(); 75 | std::cout<<”do_something() took “ 76 | <(stop-start).count() 77 | <<” seconds”<`实例的时钟参数可不仅是能够指定unix时间戳的。当你想一个等待函数(绝对时间超时的方式)传递时间点时,时间点的时钟参数就被用来测量时间。当时钟变更时,会产生严重的后果,因为等待轨迹随着时钟的改变而改变,并且知道调用时钟的now()成员函数时,才能返回一个超过超时时间的值。当时钟向前调整,这就有可能减小等待时间的总长度(与稳定时钟的测量相比);当时钟向后调整,就有可能增加等待时间的总长度。 81 | 82 | 如你期望的那样,后缀为_unitl的(等待函数的)变量会使用时间点。通常是使用某些时钟的`::now()`(程序中一个固定的时间点)作为偏移,虽然时间点与系统时钟有关,可以使用`std::chrono::system_clock::to_time_point()` 静态成员函数,在用户可视时间点上进行调度操作。例如,当你有一个对多等待500毫秒的,且与条件变量相关的事件,你可以参考如下代码: 83 | 84 | 清单4.11 等待一个条件变量——有超时功能 85 | 86 | ``` 87 | #include 88 | #include 89 | #include 90 | 91 | std::condition_variable cv; 92 | bool done; 93 | std::mutex m; 94 | 95 | bool wait_loop() 96 | { 97 | auto const timeout= std::chrono::steady_clock::now()+ 98 | std::chrono::milliseconds(500); 99 | std::unique_lock lk(m); 100 | while(!done) 101 | { 102 | if(cv.wait_until(lk,timeout)==std::cv_status::timeout) 103 | break; 104 | } 105 | return done; 106 | } 107 | ``` 108 | 109 | 这种方式是我们推荐的,当你没有什么事情可以等待时,可在一定时限中等待条件变量。在这种方式中,循环的整体长度是有限的。如你在4.1.1节中所见,当使用条件变量(且无事可待)时,你就需要使用循环,这是为了处理假唤醒。当你在循环中使用wait_for()时,你可能在等待了足够长的时间后结束等待(在假唤醒之前),且下一次等待又开始了。这可能重复很多次,使得等待时间无边无际。 110 | 111 | 到此,有关时间点超时的基本知识你已经了解了。现在,让我们来了解一下如何在函数中使用超时。 112 | 113 | ### 4.3.4 具有超时功能的函数 114 | 115 | 使用超时的最简单方式就是,对一个特定线程添加一个延迟处理;当这个线程无所事事时,就不会占用可供其他线程处理的时间。你在4.1节中看过一个例子,你循环检查“done”标志。两个处理函数分别是`std::this_thread::sleep_for()`和`std::this_thread::sleep_until()`。他们的工作就像一个简单的闹钟:当线程因为指定时延而进入睡眠时,可使用sleep_for()唤醒;或因指定时间点睡眠的,可使用sleep_until唤醒。sleep_for()的使用如同在4.1节中的例子,有些事必须在指定时间范围内完成,所以耗时在这里就很重要。另一方面,sleep_until()允许在某个特定时间点将调度线程唤醒。这有可能在晚间备份,或在早上6:00打印工资条时使用,亦或挂起线程直到下一帧刷新时进行视频播放。 116 | 117 | 当然,休眠只是超时处理的一种形式;你已经看到了,超时可以配合条件变量和“期望”一起使用。超时甚至可以在尝试获取一个互斥锁时(当互斥量支持超时时)使用。`std::mutex`和`std::recursive_mutex`都不支持超时锁,但是`std::timed_mutex`和`std::recursive_timed_mutex`支持。这两种类型也有try_lock_for()和try_lock_until()成员函数,可以在一段时期内尝试,或在指定时间点前获取互斥锁。表4.1展示了C++标准库中支持超时的函数。参数列表为“延时”(*duration*)必须是`std::duration<>`的实例,并且列出为*时间点*(time_point)必须是`std::time_point<>`的实例。 118 | 119 | 表4.1 可接受超时的函数 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 |
类型/命名空间函数返回值
std::this_thread[namespace] sleep_for(duration) N/A
sleep_until(time_point)
std::condition_variable 或 std::condition_variable_anywait_for(lock, duration)std::cv_status::time_out 或 std::cv_status::no_timeout
wait_until(lock, time_point)
wait_for(lock, duration, predicate)bool —— 当唤醒时,返回谓词的结果
wait_until(lock, duration, predicate)
std::timed_mutex 或 std::recursive_timed_mutextry_lock_for(duration) bool —— 获取锁时返回true,否则返回fasle
try_lock_until(time_point)
std::unique_lock<TimedLockable>unique_lock(lockable, duration)N/A —— 对新构建的对象调用owns_lock();
unique_lock(lockable, time_point)当获取锁时返回true,否则返回false
try_lock_for(duration)bool —— 当获取锁时返回true,否则返回false
try_lock_until(time_point)
std::future<ValueType>或std::shared_future<ValueType>wait_for(duration)当等待超时,返回std::future_status::timeout
wait_until(time_point)当“期望”准备就绪时,返回std::future_status::ready
当“期望”持有一个为启动的延迟函数,返回std::future_status::deferred
187 | 188 | 现在,我们讨论的机制有:条件变量、“期望”、“承诺”还有打包的任务。是时候从更高的角度去看待这些机制,怎么样使用这些机制,简化线程的同步操作。 -------------------------------------------------------------------------------- /content/chapter4/4.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 4.4 使用同步操作简化代码 2 | 3 | 同步工具的使用在本章称为构建块,你可以之关注那些需要同步的操作,而非具体使用的机制。当需要为程序的并发时,这是一种可以帮助你简化你的代码的方式,提供更多的函数化的方法。比起在多个线程间直接共享数据,每个任务拥有自己的数据会应该会更好,并且结果可以对其他线程进行广播,这就需要使用“期望”来完成了。 4 | 5 | ### 4.4.1 使用“期望”的函数化编程 6 | 7 | 术语*函数化编程*(functional programming)引用于一种编程方式,这种方式中的函数结果只依赖于传入函数的参数,并不依赖外部状态。当一个函数与数学概念相关时,当你使用相同的函数调用这个函数两次,这两次的结果会完全相同。`C++`标准库中很多与数学相关的函数都有这个特性,例如,sin(正弦),cos(余弦)和sqrt(平方根);当然,还有基本类型间的简单运算,例如,3+3,6*9,或1.3/4.7。一个纯粹的函数不会改变任何外部状态,并且这种特性完全限制了函数的返回值。 8 | 9 | 很容易想象这是一种什么样的情况,特别是当并行发生时,因为在第三章时我们讨论过,很多问题发生在共享数据上。当共享数据没有被修改,那么就不存在条件竞争,并且没有必要使用互斥量去保护共享数据。这可对编程进行极大的简化,例如Haskell语言[2],在Haskell中函数默认就是这么的“纯粹”;这种纯粹对的方式,在并发编程系统中越来越受欢迎。因为大多数函数都是纯粹的,那么非纯粹的函数对共享数据的修改就显得更为突出,所以其很容易适应应用的整体结构。 10 | 11 | 函数化编程的好处,并不限于那些将“纯粹”作为默认方式(范型)的语言。`C++`是一个多范型的语言,其也可以写出FP类型的程序。在`C++11`中这种方式要比`C++98`简单许多,因为`C++11`支持lambda表达式(详见附录A,A.6节),还加入了[Boost](http://zh.wikipedia.org/wiki/Boost_C%2B%2B_Libraries)和[TR1](http://zh.wikipedia.org/wiki/C%2B%2B_Technical_Report_1)中的`std::bind`,以及自动可以自行推断类型的自动变量(详见附录A,A.7节)。“期望”作为拼图的最后一块,它使得*函数化编程模式并发化*(FP-style concurrency)在`C++`中成为可能;一个“期望”对象可以在线程间互相传递,并允许其中一个计算结果依赖于另外一个的结果,而非对共享数据的显式访问。 12 | 13 | **快速排序 FP模式版** 14 | 15 | 为了描述在*函数化*(PF)并发中使用“期望”,让我们来看看一个简单的实现——快速排序算法。该算法的基本思想很简单:给定一个数据列表,然后选取其中一个数为“中间”值,之后将列表中的其他数值分成两组——一组比中间值大,另一组比中间值小。之后对小于“中间”值的组进行排序,并返回排序好的列表;再返回“中间”值;再对比“中间”值大的组进行排序,并返回排序的列表。图4.2中展示了10个整数在这种方式下进行排序的过程。 16 | 17 | ![](../../images/chapter4/4-2.png) 18 | 19 | 图4.2 FP-模式的递归排序 20 | 21 | 下面清单中的代码是FP-模式的顺序实现,它需要传入列表,并且返回一个列表,而非与`std::sort()`做同样的事情。 22 | (译者:`std::sort()`是无返回值的,因为参数接收的是迭代器,所以其可以对原始列表直进行修改与排序。可参考[sort()](http://www.cplusplus.com/reference/algorithm/sort/?kw=sort)) 23 | 24 | 清单4.12 快速排序——顺序实现版 25 | 26 | ``` 27 | template 28 | std::list sequential_quick_sort(std::list input) 29 | { 30 | if(input.empty()) 31 | { 32 | return input; 33 | } 34 | std::list result; 35 | result.splice(result.begin(),input,input.begin()); // 1 36 | T const& pivot=*result.begin(); // 2 37 | 38 | auto divide_point=std::partition(input.begin(),input.end(), 39 | [&](T const& t){return t lower_part; 42 | lower_part.splice(lower_part.end(),input,input.begin(), 43 | divide_point); // 4 44 | auto new_lower( 45 | sequential_quick_sort(std::move(lower_part))); // 5 46 | auto new_higher( 47 | sequential_quick_sort(std::move(input))); // 6 48 | 49 | result.splice(result.end(),new_higher); // 7 50 | result.splice(result.begin(),new_lower); // 8 51 | return result; 52 | } 53 | ``` 54 | 55 | 虽然接口的形式是FP模式的,但当你使用FP模式时,你需要做大量的拷贝操作,所以在内部你会使用“普通”的命令模式。你选择第一个数为“中间”值,使用splice()①将输入的首个元素(中间值)放入结果列表中。虽然这种方式产生的结果可能不是最优的(会有大量的比较和交换操作),但是对`std::list`做任何事都需要花费较长的时间,因为链表是遍历访问的。你知道你想要什么样的结果,所以你可以直接将要使用的“中间”值提前进行拼接。现在你还需要使用“中间”值进行比较,所以这里使用了一个引用②,为了避免过多的拷贝。之后,你可以使用`std::partition`将序列中的值分成小于“中间”值的组和大于“中间”值的组③。最简单的方法就是使用lambda函数指定区分的标准;使用已获取的引用避免对“中间”值的拷贝(详见附录A,A.5节,更多有关lambda函数的信息)。 56 | 57 | `std::partition()`对列表进行重置,并返回一个指向首元素(*不*小于“中间”值)的迭代器。迭代器的类型全称可能会很长,所以你可以使用auto类型说明符,让编译器帮助你定义迭代器类型的变量(详见附录A,A.7节)。 58 | 59 | 现在,你已经选择了FP模式的接口;所以,当你要使用递归对两部分排序是,你将需要创建两个列表。你可以用splice()函数来做这件事,将input列表小于divided_point的值移动到新列表lower_part④中。其他数继续留在input列表中。而后,你可以使用递归调用⑤⑥的方式,对两个列表进行排序。这里显式使用`std::move()`将列表传递到类函数中,这种方式还是为了避免大量的拷贝操作。最终,你可以再次使用splice(),将result中的结果以正确的顺序进行拼接。new_higher指向的值放在“中间”值的后面⑦,new_lower指向的值放在“中间”值的前面⑧。 60 | 61 | **快速排序 FP模式线程强化版** 62 | 63 | 因为还是使用函数化模式,所以使用“期望”很容易将其转化为并行的版本,如下面的程序清单所示。其中的操作与前面相同,不同的是它们现在并行运行。 64 | 65 | 清单4.13 快速排序——“期望”并行版 66 | 67 | ``` 68 | template 69 | std::list parallel_quick_sort(std::list input) 70 | { 71 | if(input.empty()) 72 | { 73 | return input; 74 | } 75 | std::list result; 76 | result.splice(result.begin(),input,input.begin()); 77 | T const& pivot=*result.begin(); 78 | 79 | auto divide_point=std::partition(input.begin(),input.end(), 80 | [&](T const& t){return t lower_part; 83 | lower_part.splice(lower_part.end(),input,input.begin(), 84 | divide_point); 85 | 86 | std::future > new_lower( // 1 87 | std::async(¶llel_quick_sort,std::move(lower_part))); 88 | 89 | auto new_higher( 90 | parallel_quick_sort(std::move(input))); // 2 91 | 92 | result.splice(result.end(),new_higher); // 3 93 | result.splice(result.begin(),new_lower.get()); // 4 94 | return result; 95 | } 96 | ``` 97 | 98 | 这里最大的变化是,当前线程不对小于“中间”值部分的列表进行排序,使用`std::async()`①在另一线程对其进行排序。大于部分列表,如同之前一样,使用递归的方式进行排序②。通过递归调用parallel_quick_sort(),你就可以利用可用的硬件并发了。`std::async()`会启动一个新线程,这样当你递归三次时,就会有八个线程在运行了;当你递归十次(对于大约有1000个元素的列表),如果硬件能处理这十次递归调用,你将会创建1024个执行线程。当运行库认为这样做产生了太多的任务时(也许是因为数量超过了硬件并发的最大值),运行库可能会同步的切换新产生的任务。当任务过多时(已影响性能),这些任务应该在使用get()函数获取的线程上运行,而不是在新线程上运行,这样就能避免任务向线程传递的开销。值的注意的是,这完全符合`std::async`的实现,为每一个任务启动一个线程(甚至在任务超额时;在`std::launch::deferred`没有明确规定的情况下);或为了同步执行所有任务(在`std::launch::async`有明确规定的情况下)。当你依赖运行库的自动缩放,建议你去查看一下你的实现文档,了解一下将会有怎么样的行为表现。 99 | 100 | 比起使用`std::async()`,你可以写一个spawn_task()函数对`std::packaged_task`和`std::thread`做简单的包装,如清单4.14中的代码所示;你需要为函数结果创建一个`std::packaged_task`对象, 可以从这个对象中获取“期望”,或在线程中执行它,返回“期望”。其本身并不提供太多的好处(并且事实上会造成大规模的超额任务),但是它会为转型成一个更复杂的实现铺平道路,将会实现向一个队列添加任务,而后使用线程池的方式来运行它们。我们将在第9章再讨论线程池。使用`std::async`更适合于当你知道你在干什么,并且要完全控制在线程池中构建或执行过任务的线程。 101 | 102 | 清单4.14 spawn_task的简单实现 103 | 104 | ``` 105 | template 106 | std::future::type> 107 | spawn_task(F&& f,A&& a) 108 | { 109 | typedef std::result_of::type result_type; 110 | std::packaged_task 111 | task(std::move(f))); 112 | std::future res(task.get_future()); 113 | std::thread t(std::move(task),std::move(a)); 114 | t.detach(); 115 | return res; 116 | } 117 | ``` 118 | 119 | 其他先不管,回到parallel_quick_sort函数。因为你只是直接递归去获取new_higher列表,你可以如之前一样对new_higher进行拼接③。但是,new_lower列表是`std::future>`的实例,而非是一个简单的列表,所以你需要调用get()成员函数在调用splice()④之前去检索数值。在这之后,等待后台任务完成,并且将结果移入splice()调用中;get()返回一个包含结果的右值引用,所以这个结果是可以移出的(详见附录A,A.1.1节,有更多有关右值引用和移动语义的信息)。 120 | 121 | 即使假设,使用`std::async()`是对可用硬件并发最好的选择,但是这样的并行实现对于快速排序来说,依然不是最理想的。其中,`std::partition`做了很多工作,即使做了依旧是顺序调用,但就现在的情况来说,已经足够好了。如果你对实现最快并行的可能性感兴趣的话,你可以去查阅一些学术文献。 122 | 123 | 因为避开了共享易变数据,函数化编程可算作是并发编程的范型;并且也是*通讯顺序进程*(CSP,Communicating Sequential Processer[3],)的范型,这里线程理论上是完全分开的,也就是没有共享数据,但是有通讯通道允许信息在不同线程间进行传递。这种范型被[Erlang语言](http://www.erlang.org)所采纳,并且在[MPI](http://www.mpi-forum.org)(*Message Passing Interface*,消息传递接口)上常用来做C和`C++`的高性能运算。现在你应该不会在对学习它们而感到惊奇了吧,因为只需遵守一些约定,`C++`就能支持它们;在接下来的一节中,我们会讨论实现这种方式。 124 | 125 | ### 4.4.2 使用消息传递的同步操作 126 | 127 | CSP的概念十分简单:当没有共享数据,每个线程就可以进行独立思考,其行为纯粹基于其所接收到的信息。每个线程就都有一个状态机:当线程收到一条信息,它将会以某种方式更新其状态,并且可能向其他线程发出一条或多条信息,对于消息的处理依赖于线程的初始化状态。这是一种正式写入这些线程的方式,并且以有限状态机的模式实现,但是这不是唯一的方案;状态机可以在应用程序中隐式实现。这种方法咋任何给定的情况下,都更加依赖于特定情形下明确的行为要求和编程团队的专业知识。无论你选择用什么方式去实现每个线程,任务都会分成独立的处理部分,这样会消除潜在的混乱(数据共享并发),这样就让编程变的更加简单,且拥有低错误率。 128 | 129 | 真正通讯顺序处理是没有共享数据的,所有消息都是通过消息队列传递,但是因为`C++`线程共享一块地址空间,所以达不到真正通讯顺序处理的要求。这里就需要有一些约定了:作为一款应用或者是一个库的作者,我们有责任确保在我们的实现中,线程不存在共享数据。当然,为了线程间的通信,消息队列是必须要共享的,具体的细节可以包含在库中。 130 | 131 | 试想,有一天你要为实现ATM(自动取款机)写一段代码。这段代码需要处理,人们尝试取钱时和银行之间的交互情况,以及控制物理器械接受用户的卡片,显示适当的信息,处理按钮事件,吐出现金,还有退还用户的卡。 132 | 133 | 一种处理所有事情的方法是让代码将所有事情分配到三个独立线程上去:一个线程去处理物理机械,一个去处理ATM机的逻辑,还有一个用来与银行通讯。这些线程可以通过信息进行纯粹的通讯,而非共享任何数据。比如,当有人在ATM机上插入了卡片或者按下按钮,处理物理机械的线程将会发送一条信息到逻辑线程上,并且逻辑线程将会发送一条消息到机械线程,告诉机械线程可以分配多少钱,等等。 134 | 135 | 一种为ATM机逻辑建模的方式是将其当做一个状态机。线程的每一个状态都会等待一条可接受的信息,这条信息包含需要处理的内容。这可能会让线程过渡到一个新的状态,并且循环继续。在图4.3中将展示,有状态参与的一个简单是实现。在这个简化实现中,系统在等待一张卡插入。当有卡插入时,系统将会等待用户输入它的PIN(类似身份码的东西),每次输入一个数字。用户可以将最后输入的数字删除。当数字输入完成,PIN就需要验证。当验证有问题,你的程序就需要终止,就需要为用户退出卡,并且继续等待其他人将卡插入到机器中。当PIN验证通过,你的程序要等待用户取消交易或选择取款。当用户选择取消交易,你的程序就可以结束,并返还卡片。当用户选择取出一定量的现金,你的程序就要在吐出现金和返还卡片前等待银行方面的确认,或显示“余额不足”的信息,并返还卡片。很明显,一个真正的ATM机要考虑的东西更多、更复杂,但是我们来说这样描述已经足够了。 136 | 137 | ![](../../images/chapter4/4-3.png) 138 | 139 | 图4.3 一台ATM机的状态机模型(简化) 140 | 141 | 我们已经为你的ATM机逻辑设计了一个状态机,你可以使用一个类实现它,这个类中有一个成员函数可以代表每一个状态。每一个成员函数可以等待从指定集合中传入的信息,以及当他们到达时进行处理,这就有可能触发原始状态向另一个状态的转化。每种不同的信息类型由一个独立的struct表示。清单4.15展示了ATM逻辑部分的简单实现(在以上描述的系统中,有主循环和对第一状态的实现),并且一直在等待卡片插入。 142 | 143 | 如你所见,所有信息传递所需的的同步,完全包含在“信息传递”库中(基本实现在附录C中,是清单4.15代码的完整版) 144 | 145 | 清单4.15 ATM逻辑类的简单实现 146 | 147 | ``` 148 | struct card_inserted 149 | { 150 | std::string account; 151 | }; 152 | 153 | class atm 154 | { 155 | messaging::receiver incoming; 156 | messaging::sender bank; 157 | messaging::sender interface_hardware; 158 | void (atm::*state)(); 159 | 160 | std::string account; 161 | std::string pin; 162 | 163 | void waiting_for_card() // 1 164 | { 165 | interface_hardware.send(display_enter_card()); // 2 166 | incoming.wait(). // 3 167 | handle( 168 | [&](card_inserted const& msg) // 4 169 | { 170 | account=msg.account; 171 | pin=""; 172 | interface_hardware.send(display_enter_pin()); 173 | state=&atm::getting_pin; 174 | } 175 | ); 176 | } 177 | void getting_pin(); 178 | public: 179 | void run() // 5 180 | { 181 | state=&atm::waiting_for_card; // 6 182 | try 183 | { 184 | for(;;) 185 | { 186 | (this->*state)(); // 7 187 | } 188 | } 189 | catch(messaging::close_queue const&) 190 | { 191 | } 192 | } 193 | }; 194 | ``` 195 | 196 | 如之前提到的,这个实现对于实际ATM机的逻辑来说是非常简单的,但是他能让你感受到信息传递编程的方式。这里无需考虑同步和并发问题,只需要考虑什么时候接收信息和发送信息即可。为ATM逻辑所设的状态机运行在独立的线程上,与系统的其他部分一起,比如与银行通讯的接口,以及运行在独立线程上的终端接口。这种程序设计的方式被称为*参与者模式*([Actor model](http://zh.wikipedia.org/wiki/%E5%8F%83%E8%88%87%E8%80%85%E6%A8%A1%E5%BC%8F))——在系统中有很多独立的(运行在一个独立的线程上)参与者,这些参与者会互相发送信息,去执行手头上的任务,并且它们不会共享状态,除非是通过信息直接传入的。 197 | 198 | 运行从run()成员函数开始⑤,其将会初始化waiting_for_card⑥的状态,然后反复执行当前状态的成员函数(无论这个状态时怎么样的)⑦。状态函数是简易atm类的成员函数。wait_for_card函数①依旧很简单:它发送一条信息到接口,让终端显示“等待卡片”的信息②,之后就等待传入一条消息进行处理③。这里处理的消息类型只能是card_inserted类的,这里使用一个lambda函数④对其进行处理。当然,你可以传递任何函数或函数对象,去处理函数,但对于一个简单的例子来说,使用lambda表达式是最简单的方式。注意handle()函数调用是连接到wait()函数上的;当收到的信息类型与处理类型不匹配,收到的信息会被丢弃,并且线程继续等待,直到接收到一条类型匹配的消息。 199 | 200 | lambda函数自身,只是将用户的账号信息缓存到一个成员变量中去,并且清除PIN信息,再发送一条消息到硬件接口,让显示界面提示用户输入PIN,然后将线程状态改为“获取PIN”。当消息处理程序结束,状态函数就会返回,然后主循环会调用新的状态函数⑦。 201 | 202 | 如图4.3,getting_pin状态函数会负载一些,因为其要处理三个不同的信息类型。具体代码展示如下: 203 | 204 | 清单4.16 简单ATM实现中的getting_pin状态函数 205 | 206 | ``` 207 | void atm::getting_pin() 208 | { 209 | incoming.wait() 210 | .handle( // 1 211 | [&](digit_pressed const& msg) 212 | { 213 | unsigned const pin_length=4; 214 | pin+=msg.digit; 215 | if(pin.length()==pin_length) 216 | { 217 | bank.send(verify_pin(account,pin,incoming)); 218 | state=&atm::verifying_pin; 219 | } 220 | } 221 | ) 222 | .handle( // 2 223 | [&](clear_last_pressed const& msg) 224 | { 225 | if(!pin.empty()) 226 | { 227 | pin.resize(pin.length()-1); 228 | } 229 | } 230 | ) 231 | .handle( // 3 232 | [&](cancel_pressed const& msg) 233 | { 234 | state=&atm::done_processing; 235 | } 236 | ); 237 | } 238 | ``` 239 | 240 | 这次需要处理三种消息类型,所以wait()函数后面接了三个handle()函数调用①②③。每个handle()都有对应的消息类型作为模板参数,并且将消息传入一个lambda函数中(其获取消息类型作为一个参数)。因为这里的调用都被连接在了一起,wait()的实现知道它是等待一条digit_pressed消息,或是一条clear_last_pressed肖息,亦或是一条cancel_pressed消息,其他的消息类型将会被丢弃。 241 | 242 | 这次当你获取一条消息时,无需再去改变状态。比如,当你获取一条digit_pressed消息时,你仅需要将其添加到pin中,除非那些数字是最终的输入。(清单4.15中)主循环⑦将会再次调用getting_pin()去等待下一个数字(或清除数字,或取消交易)。 243 | 244 | 这里对应的动作如图4.3所示。每个状态盒的实现都由一个不同的成员函数构成,等待相关信息并适当的更新状态。 245 | 246 | 如你所见,在一个并发系统中这种编程方式可以极大的简化任务的设计,因为每一个线程都完全被独立对待。因此,在使用多线程去分离关注点时,需要你明确如何分配线程之间的任务。 247 | 248 | --------- 249 | 250 | [2] 详见 http://www.haskell.org/. 251 | 252 | [3] 《通信顺序进程》(*Communicating Sequential Processes*), C.A.R. Hoare, Prentice Hall, 1985. 免费在线阅读地址 http://www.usingcsp.com/cspbook.pdf. -------------------------------------------------------------------------------- /content/chapter4/4.5-chinese.md: -------------------------------------------------------------------------------- 1 | # 4.5 本章总结 2 | 3 | 同步操作对于使用并发编写一款多线程应用来说,是很重要的一部分:如果没有同步,线程基本上就是独立的,也可写成单独的应用,因其任务之间的相关性,它们可作为一个群体直接执行。本章,我们讨论了各式各样的同步操作,从基本的条件变量,到“期望”、“承诺”,再到打包任务。我们也讨论了替代同步的解决方案:函数化模式编程,完全独立执行的函数,不会受到外部环境的影响;还有,消息传递模式,以消息子系统为中介,向线程异步的发送消息。 4 | 5 | 我们已经讨论了很多C++中的高层工具,现在我们来看一下底层工具是如何让一切都工作的:C++内存模型和原子操作。 -------------------------------------------------------------------------------- /content/chapter5/5.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第5章 C++内存模型和原子类型操作 2 | 3 | **本章主要内容** 4 | 5 | - C++11内存模型详解
6 | - 标准库提供的原子类型
7 | - 使用各种原子类型
8 | - 原子操作实现线程同步功能
9 | 10 | C++11标准中,有一个十分重要特性,常被程序员们所忽略。它不是一个新语法特性,也不是新工具,它就是多线程(感知)内存模型。内存模型没有明确的定义基本部件应该如何工作的话,之前介绍的那些工具就无法正常工作。那为什么大多数程序员都没有注意到它呢?当你使用互斥量保护你的数据和条件变量,或者是“期望”上的信号事件时,对于互斥量*为什么*能起到这样作用,大多数人不会去关心。只有当你试图去“接触硬件”,你才能详尽的了解到内存模型是如何起作用的。 11 | 12 | C++是一个系统级别的编程语言,标准委员会的目标之一就是不需要比`C++`还要底层的高级语言。`C++`应该向程序员提供足够的灵活性,无障碍的去做他们想要做的事情;当需要的时候,可以让他们“接触硬件”。原子类型和原子操作就允许他们“接触硬件”,并提供底层级别的同步操作,通常会将常规指令数缩减到1~2个CPU指令。 13 | 14 | 本章,我们将讨论内存模型的基本知识,而后再了解一下原子类型和操作,最后了解与原子类型操作相关的各种同步。这个过程会比较复杂:除非你已经打算使用原子操作(比如,第7章的无锁数据结构)同步你的代码,否则,就没有必要了解过多的细节。 15 | 16 | 让我们先轻松愉快的来看一下有关内存模型的基本知识。 -------------------------------------------------------------------------------- /content/chapter5/5.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 5.1 内存模型基础 2 | 3 | 这里从两方面来讲内存模型:一方面是基本结构,这与事务在内存中是怎样布局的有关;另一方面就是并发。对于并发基本结构很重要,特别是在低层原子操作。所以我将会从基本结构讲起。`C++`中它与所有的对象和内存位置有关。 4 | 5 | ## 5.1.1 对象和内存位置 6 | 7 | 在一个`C++`程序中的所有数据都是由对象(objects)构成。这不是说你可以创建一个int的衍生类,或者是基本类型中存在有成员函数,或是像在Smalltalk和Ruby语言下讨论程序那样——“一切都是对象”。“对象”仅仅是对C++数据构建块的一个声明。`C++`标准定义类对象为“存储区域”,但对象还是可以将自己的特性赋予其他对象,比如,其类型和生命周期。 8 | 9 | 像int或float这样的对象就是简单基本类型;当然,也有用户定义类的实例。一些对象(比如,数组,衍生类的实例,特殊(具有非静态数据成员)类的实例)拥有子对象,但是其他对象就没有。 10 | 11 | 无论对象是怎么样的一个类型,一个对象都会存储在一个或多个内存位置上。每一个内存位置不是一个标量类型的对象,就是一个标量类型的子对象,比如,unsigned short、my_class*或序列中的相邻位域。当你使用位域,就需要注意:虽然相邻位域中是不同的对象,但仍视其为相同的内存位置。如图5.1所示,将一个struct分解为多个对象,并且展示了每个对象的内存位置。 12 | 13 | ![](../../images/chapter5/5-1.png) 14 | 15 | 图5.1 分解一个struct,展示不同对象的内存位置 16 | 17 | 首先,完整的struct是一个有多个子对象(每一个成员变量)组成的对象。位域bf1和bf2共享同一个内存位置(int是4字节、32位类型),并且`std::string`类型的对象s由内部多个内存位置组成,但是其他的每个成员都拥有自己的内存位置。注意,位域宽度为0的bf3是如何与bf4分离,并拥有各自的内存位置的。(译者注:图中bf3是一个错误展示,在`C++`和C中规定,宽度为0的一个未命名位域强制下一位域对齐到其下一type边界,其中type是该成员的类型。这里使用命名变量为0的位域,可能只是想展示其与bf4是如何分离的。有关位域的更多可以参考[wiki](https://en.wikipedia.org/wiki/Bit_field)的页面)。 18 | 19 | 这里有四个需要牢记的原则:
20 | 21 | 1. 每一个变量都是一个对象,包括作为其成员变量的对象。
22 | 2. 每个对象至少占有一个内存位置。
23 | 3. 基本类型都有确定的内存位置(无论类型大小如何,即使他们是相邻的,或是数组的一部分)。
24 | 4. 相邻位域是相同内存中的一部分。
25 | 26 | 我确定你会好奇,这些在并发中有什么作用,那么下面就让我们来见识一下。 27 | 28 | ## 5.1.2 对象、内存位置和并发 29 | 30 | 这部分对于`C++`的多线程应用来说是至关重要的:所有东西都在内存中。当两个线程访问不同的内存位置时,不会存在任何问题,一切都工作顺利。而另一种情况下,当两个线程访问同一个内存位置,你就要小心了。如果没有线程更新内存位置上的数据,那还好;只读数据不需要保护或同步。当有线程对内存位置上的数据进行修改,那就有可能会产生条件竞争,就如第3章所述的那样。 31 | 32 | 为了避免条件竞争,两个线程就需要一定的执行顺序。第一种方式,如第3章所述那样,使用互斥量来确定访问的顺序;当同一互斥量在两个线程同时访问前被锁住,那么在同一时间内就只有一个线程能够访问到对应的内存位置,所以后一个访问必须在前一个访问之后。另一种方式是使用原子操作同步机制(详见5.2节中对于原子操作的定义),决定两个线程的访问顺序。使用原子操作来规定顺序在5.3节中会有介绍。当多于两个线程访问同一个内存地址时,对每个访问这都需要定义一个顺序。 33 | 34 | 如果不去规定两个不同线程对同一内存地址访问的顺序,那么访问就不是原子的;并且,当两个线程都是“作者”时,就会产生数据竞争和未定义行为。 35 | 36 | 以下的声明由为重要:未定义的行为是`C++`中最黑暗的角落。根据语言的标准,一旦应用中有任何未定义的行为,就很难预料会发生什么事情;因为,未定义行为是难以预料的。我就知道一个未定义行为的特定实例,让某人的显示器起火的案例。虽然,这种事情应该不会发生在你身上,但是数据竞争绝对是一个严重的错误,并且需要不惜一切代价避免它。 37 | 38 | 另一个重点是:当程序中的对同一内存地址中的数据访问存在竞争,你可以使用原子操作来避免未定义行为。当然,这不会影响竞争的产生——原子操作并没有指定访问顺序——但原子操作把程序拉回了定义行为的区域内。 39 | 40 | 在我们了解原子操作前,还有一个有关对象和内存地址的概念需要重点了解:修改顺序。 41 | 42 | ## 5.1.3 修改顺序 43 | 44 | 每一个在C++程序中的对象,都有(由程序中的所有线程对象)确定好的修改顺序,在的初始化开始阶段确定。在大多数情况下,这个顺序不同于执行中的顺序,但是在给定的执行程序中,所有线程都需要遵守这顺序。如果对象不是一个原子类型(将在5.2节详述),你必要确保有足够的同步操作,来确定每个线程都遵守了变量的修改顺序。当不同线程在不同序列中访问同一个值时,你可能就会遇到数据竞争或未定义行为(详见5.1.2节)。如果你使用原子操作,编译器就有责任去替你做必要的同步。 45 | 46 | 这一要求意味着:投机执行是不允许的,因为当线程按修改顺序访问一个特殊的输入,之后的读操作,必须由线程返回较新的值,并且之后的写操作必须发生在修改顺序之后。同样的,在同一线程上允许读取对象的操作,要不返回一个已写入的值,要不在对象的修改顺序后(也就是在读取后)再写入另一个值。虽然,所有线程都需要遵守程序中每个独立对象的修改顺序,但它们没有必要遵守在独立对象上的相对操作顺序。在5.3.3节中会有更多关于不同线程间操作顺序的内容。 47 | 48 | 所以,什么是原子操作?它如何来规定顺序?接下来的一节中,会为你揭晓答案。 49 | 50 | -------------------------------------------------------------------------------- /content/chapter5/5.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 5.4 本章总结 2 | 3 | 在本章中,已经对`C++`11内存模型的底层只是进行详尽的了解,并且了解了原子操作能在线程间提供基本的同步。这里包含基本的原子类型,由`std::atomic<>`类模板特化后提供;接口,以及对于这些类型的操作,还要有对内存序列选项的各种复杂细节,都由原始`std::atomic<>`类模板提供。 4 | 5 | 我们也了解了栅栏,了解其如何让执行序列中,对原子类型的操作同步成对。最后,我们回顾了本章开始的一些例子,了解了原子操作可以在不同线程上的非原子操作间,进行有序执行。 6 | 7 | 在下一章中,我们将看到如何使用高阶同步工具,以及原子操作并发访问的高效容器设计,还有我们将写一些并行处理数据的算法。 -------------------------------------------------------------------------------- /content/chapter6/6.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第6章 基于锁的并发数据结构设计 2 | 3 | **本章主要内容** 4 | 5 | - 并发数据结构设计的意义
6 | - 指导如何设计
7 | - 实现为并发设计的数据结构
8 | 9 | 在上一章中,我们对底层原子操作和内存模型有了详尽的了解。在本章中,我们将先将底层的东西放在一边(将会在第7章再次提及),来对数据结构做一些讨论。 10 | 11 | 数据结构的选择,对于程序来说,是其解决方案的重要组成部分,当然,并行程序也不例外。如果一种数据结构可以被多个线程所访问,其要不就是绝对不变的(其值不会发生变化,并且不需同步),要不程序就要对数据结构进行正确的设计,以确保其能在多线程环境下能够(正确的)同步。一种选择是使用独立的互斥量,其可以锁住需要保护的数据(这种方法已经在第3和第4章中提到),另一种选择是设计一种能够并发访问的数据结构。 12 | 13 | 在设计并发数据结构时,你可以使用基本多线程应用中的构建块(之前章节中有提及),比如,互斥量和条件变量。当然,你也已经在之前的章节的例子中看到,怎样联合不同的构建块,对数据结构进行写入,并且保证这些构建块都是在并发环境下是线程安全的。 14 | 15 | 在本章,我们将了解一些并发数据结构设计的基本准则。然后,我们将再次重温锁和条件变量的基本构建块。最后,会去了解更为复杂的数据结构。在第7章,我们将了解,如何正确的“返璞归真”,并使用第5章提到的原子操作,去构建无锁的数据结构。 16 | 17 | 好吧!多说无益,让我们来看一下并发数据结构的设计,都需要些*什么*。 -------------------------------------------------------------------------------- /content/chapter6/6.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 6.1 为并发设计的意义何在? 2 | 3 | 设计并发数据结构,意味着多个线程可以并发的访问这个数据结构,线程可对这个数据结构做相同或不同的操作,并且每一个线程都能在自己的自治域中看到该数据结构。且在多线程环境下,无数据丢失和损毁,所有的数据需要维持原样,且无条件竞争。这样的数据结构,称之为“线程安全”的数据结构。通常情况下,当多个线程对数据结构进行同一并发操作是安全的,但不同操作则需要单线程独立访问数据结构。或相反,当线程执行不同的操作时,对同一数据结构的并发操作是安全的,而多线程执行同样的操作,则会出现问题。 4 | 5 | 实际的设计意义并不止上面提到的那样:这就意味着,要为线程提供并发访问数据结构的机会。本质上,是使用互斥量提供互斥特性:在互斥量的保护下,同一时间内只有一个线程可以获取互斥锁。互斥量为了保护数据,显式的阻止了线程对数据结构的并发访问。 6 | 7 | 这被称为*串行化*(serialzation):线程轮流访问被保护的数据。这其实是对数据进行串行的访问,而非并发。因此,你需要对数据结构的设计进行仔细斟酌,确保其能真正并发访问。虽然,一些数据结构有着比其他数据结构多的并发访问范围,但是在所有情况下的思路都是一样的:减少保护区域,减少序列化操作,就能提升并发访问的潜力。 8 | 9 | 在我们进行数据结构的设计之前,让我们快速的浏览一下,在并发设计中的指导建议。 10 | 11 | ## 6.1.1 数据结构并发设计的指导与建议(指南) 12 | 13 | 如之前提到的,当设计并发数据结构时,有两方面需要考量:一是确保访问是安全的,二是能真正的并发访问。在第3章的时候,已经对如何保证数据结构是线程安全的做过简单的描述: 14 | 15 | - 确保无线程能够看到,数据结构的“不变量”破坏时的状态。 16 | 17 | - 小心那些会引起条件竞争的接口,提供完整操作的函数,而非操作步骤。 18 | 19 | - 注意数据结构的行为是否会产生异常,从而确保“不变量”的状态稳定。 20 | 21 | - 将死锁的概率降到最低。使用数据结构时,需要限制锁的范围,且避免嵌套锁的存在。 22 | 23 | 在你思考设计细节前,你还需要考虑这个数据结构对于使用者来说有什么限制;当一个线程通过一个特殊的函数对数据结构进行访问时,那么还有哪些函数能被其他的线程安全调用呢? 24 | 25 | 这是一个很重要的问题,普通的构造函数和析构函数需要独立访问数据结构,所以用户在使用的时候,就不能在构造函数完成前,或析构函数完成后对数据结构进行访问。当数据结构支持赋值操作,swap(),或拷贝构造时,作为数据结构的设计者,即使数据结构中有大量的函数被线程所操纵时,你也需要保证这些操作在并发环境下是安全的(或确保这些操作能够独立访问),以保证并发访问时不会出现错误。 26 | 27 | 第二个方面是,确保真正的并发访问。这里没法提供更多的指导意见;不过,作为一个数据结构的设计者,在设计数据结构时,自行考虑以下问题: 28 | 29 | - 锁的范围中的操作,是否允许在锁外执行? 30 | 31 | - 数据结构中不同的区域是否能被不同的互斥量所保护? 32 | 33 | - 所有操作都需要同级互斥量保护吗? 34 | 35 | - 能否对数据结构进行简单的修改,以增加并发访问的概率,且不影响操作语义? 36 | 37 | 这些问题都源于一个指导思想:如何让序列化访问最小化,让真实并发最大化?允许线程并发读取的数据结构并不少见,而对数据结构的修改,必须是单线程独立访问。这种结构,类似于`boost::shared_mutex`。同样的,这种数据结构也很常见——支持在多线程执行不同的操作时,并序列化执行相同的操作的线程(你很快就能看到)。 38 | 39 | 最简单的线程安全结构,通常使用的是互斥量和锁,对数据进行保护。虽然,这么做还是有问题(如同在第3中提到的那样),不过这样相对简单,且保证只有一个线程在同一时间对数据结构进行一次访问。为了让你轻松的设计线程安全的数据结构,接下来了解一下基于锁的数据结构,以及第7章将提到的无锁并发数据结构的设计。 40 | -------------------------------------------------------------------------------- /content/chapter6/6.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 6.3 基于锁设计更加复杂的数据结构 2 | 3 | 栈和队列都很简单:接口相对固定,并且它们应用于比较特殊的情况。并不是所有数据结构都像它们一样简单;大多数数据结构支持更加多样化的操作。原则上,这将增大并行的可能性,但是也让对数据保护变得更加困难,因为要考虑对所有能访问到的部分。当为了并发访问对数据结构进行设计时,这一系列原有的操作,就变得越发重要,需要重点处理。 4 | 5 | 先来看看,在查询表的设计中,所遇到的一些问题。 6 | 7 | ## 6.3.1 编写一个使用锁的线程安全查询表 8 | 9 | 查询表或字典是一种类型的值(键值)和另一种类型的值进行关联(映射的方式)。一般情况下,这样的结构允许代码通过键值对相关的数据值进行查询。在`C++`标准库中,这种相关工具有:`std::map<>`, `std::multimap<>`, `std::unordered_map<>`以及`std::unordered_multimap<>`。 10 | 11 | 查询表的使用与栈和队列不同。栈和队列上,几乎每个操作都会对数据结构进行修改,不是添加一个元素,就是删除一个,而对于查询表来说,几乎不需要什么修改。清单3.13中有个例子,是一个简单的域名系统(DNS)缓存,其特点是,相较于`std::map<>`削减了很多的接口。和队列和栈一样,标准容器的接口不适合多线程进行并发访问,因为这些接口在设计的时候都存在固有的条件竞争,所以这些接口需要砍掉,以及重新修订。 12 | 13 | 并发访问时,`std::map<>`接口最大的问题在于——迭代器。虽然,在多线程访问(或修改)容器时,可能会有提供安全访问的迭代器,但这就问题棘手之处。要想正确的处理迭代器,你可能会碰到下面这个问题:当迭代器引用的元素被其他线程删除时,迭代器在这里就是个问题了。线程安全的查询表,第一次接口削减,需要绕过迭代器。`std::map<>`(以及标准库中其他相关容器)给定的接口对于迭代器的依赖是很严重的,其中有些接口需要先放在一边,先对一些简单接口进行设计。 14 | 15 | 查询表的基本操作有: 16 | 17 | - 添加一对“键值-数据” 18 | 19 | - 修改指定键值所对应的数据 20 | 21 | - 删除一组值 22 | 23 | - 通过给定键值,获取对应数据 24 | 25 | 容器也有一些操作是非常有用的,比如:查询容器是否为空,键值列表的完整快照和“键值-数据”的完整快照。 26 | 27 | 如果你坚持之前的线程安全指导意见,例如:不要返回一个引用,并且用一个简单的互斥锁对每一个成员函数进行上锁,以确保每一个函数线程安全。最有可能的条件竞争在于,当一对“键值-数据”加入时;当两个线程都添加一个数据,那么肯定一个先一个后。一种方式是合并“添加”和“修改”操作,为一个成员函数,就像清单3.13对域名系统缓存所做的那样。 28 | 29 | 从接口角度看,有一个问题很是有趣,那就是*任意*(if any)部分获取相关数据。一种选择是允许用户提供一个“默认”值,在键值没有对应值的时候进行返回: 30 | 31 | ``` 32 | mapped_type get_value(key_type const& key, mapped_type default_value); 33 | ``` 34 | 35 | 在种情况下,当default_value没有明确的给出时,默认构造出的mapped_type实例将被使用。也可以扩展成返回一个`std::pair`来代替mapped_type实例,其中bool代表返回值是否是当前键对应的值。另一个选择是,返回一个有指向数据的智能指针;当指针的值是NULL时,那么这个键值就没有对应的数据。 36 | 37 | 如我们之前所提到的,当接口确定时,那么(假设没有接口间的条件竞争)就需要保证线程安全了,可以通过对每一个成员函数使用一个互斥量和一个简单的锁,来保护底层数据。不过,当独立的函数对数据结构进行读取和修改时,就会降低并发的可能性。一个选择是使用一个互斥量去面对多个读者线程,或一个作者线程,如同在清单3.13中对`boost::shared_mutex`的使用一样。虽然,这将提高并发访问的可能性,但是在同一时间内,也只有一个线程能对数据结构进行修改。理想很美好,现实很骨感?我们应该能做的更好! 38 | 39 | **为细粒度锁设计一个映射结构** 40 | 41 | 在对队列的讨论中(在6.2.3节),为了允许细粒度锁能正常工作,需要对于数据结构的细节进行仔细的考虑,而非直接使用已存在的容器,例如`std::map<>`。这里列出三个常见关联容器的方式: 42 | 43 | - 二叉树,比如:红黑树 44 | 45 | - 有序数组 46 | 47 | - 哈希表 48 | 49 | 二叉树的方式,不会对提高并发访问的概率;每一个查找或者修改操作都需要访问根节点,因此,根节点需要上锁。虽然,访问线程在向下移动时,这个锁可以进行释放,但相比横跨整个数据结构的单锁,并没有什么优势。 50 | 51 | 有序数组是最坏的选择,因为你无法提前言明数组中哪段是有序的,所以你需要用一个锁将整个数组锁起来。 52 | 53 | 那么就剩哈希表了。假设有固定数量的桶,每个桶都有一个键值(关键特性),以及散列函数。这就意味着你可以安全的对每个桶上锁。当你再次使用互斥量(支持多读者单作者)时,你就能将并发访问的可能性增加N倍,这里N是桶的数量。当然,缺点也是有的:对于键值的操作,需要有合适的函数。C++标准库提供`std::hash<>`模板,可以直接使用。对于特化的类型,比如int,以及通用库类型`std::string`,并且用户可以简单的对键值类型进行特化。如果你去效仿标准无序容器,并且获取函数对象的类型作为哈希表的模板参数,用户可以选择是否特化`std::hash<>`的键值类型,或者提供一个独立的哈希函数。 54 | 55 | 那么,让我们来看一些代码吧。怎样的实现才能完成一个线程安全的查询表?下面就是一种方式。 56 | 57 | 清单6.11 线程安全的查询表 58 | 59 | ``` 60 | template > 61 | class threadsafe_lookup_table 62 | { 63 | private: 64 | class bucket_type 65 | { 66 | private: 67 | typedef std::pair bucket_value; 68 | typedef std::list bucket_data; 69 | typedef typename bucket_data::iterator bucket_iterator; 70 | 71 | bucket_data data; 72 | mutable boost::shared_mutex mutex; // 1 73 | 74 | bucket_iterator find_entry_for(Key const& key) const // 2 75 | { 76 | return std::find_if(data.begin(),data.end(), 77 | [&](bucket_value const& item) 78 | {return item.first==key;}); 79 | } 80 | public: 81 | Value value_for(Key const& key,Value const& default_value) const 82 | { 83 | boost::shared_lock lock(mutex); // 3 84 | bucket_iterator const found_entry=find_entry_for(key); 85 | return (found_entry==data.end())? 86 | default_value:found_entry->second; 87 | } 88 | 89 | void add_or_update_mapping(Key const& key,Value const& value) 90 | { 91 | std::unique_lock lock(mutex); // 4 92 | bucket_iterator const found_entry=find_entry_for(key); 93 | if(found_entry==data.end()) 94 | { 95 | data.push_back(bucket_value(key,value)); 96 | } 97 | else 98 | { 99 | found_entry->second=value; 100 | } 101 | } 102 | 103 | void remove_mapping(Key const& key) 104 | { 105 | std::unique_lock lock(mutex); // 5 106 | bucket_iterator const found_entry=find_entry_for(key); 107 | if(found_entry!=data.end()) 108 | { 109 | data.erase(found_entry); 110 | } 111 | } 112 | }; 113 | 114 | std::vector > buckets; // 6 115 | Hash hasher; 116 | 117 | bucket_type& get_bucket(Key const& key) const // 7 118 | { 119 | std::size_t const bucket_index=hasher(key)%buckets.size(); 120 | return *buckets[bucket_index]; 121 | } 122 | 123 | public: 124 | typedef Key key_type; 125 | typedef Value mapped_type; 126 | 127 | typedef Hash hash_type; 128 | threadsafe_lookup_table( 129 | unsigned num_buckets=19,Hash const& hasher_=Hash()): 130 | buckets(num_buckets),hasher(hasher_) 131 | { 132 | for(unsigned i=0;i>`⑥来保存桶,其允许在构造函数中指定构造桶的数量。默认为19个,其是一个任意的[质数](http://zh.wikipedia.org/zh-cn/%E7%B4%A0%E6%95%B0);哈希表在有质数个桶时,工作效率最高。每一个桶都会被一个`boost::shared_mutex`①实例锁保护,来允许并发读取,或对每一个桶,只有一个线程对其进行修改。 161 | 162 | 因为桶的数量是固定的,所以get_bucket()⑦可以无锁调用,⑧⑨⑩也都一样。并且对桶的互斥量上锁,要不就是共享(只读)所有权的时候③,要不就是在获取唯一(读/写)权的时候④⑤。这里的互斥量,可适用于每个成员函数。 163 | 164 | 这三个函数都使用到了find_entry_for()成员函数②,在桶上用来确定数据是否在桶中。每一个桶都包含一个“键值-数据”的`std::list<>`列表,所以添加和删除数据,就会很简单。 165 | 166 | 已经从并发的角度考虑了,并且所有成员都会被互斥锁保护,所以这样的实现就是“异常安全”的吗?value_for是不能修改任何值的,所以其不会有问题;如果value_for抛出异常,也不会对数据结构有任何影响。remove_mapping修改链表时,将会调用erase,不过这就能保证没有异常抛出,那么这里也是安全的。那么就剩add_or_update_mapping了,其可能会在其两个if分支上抛出异常。push_back是异常安全的,如果有异常抛出,其也会将链表恢复成原来的状态,所以这个分支是没有问题的。唯一的问题就是在赋值阶段,这将替换已有的数据;当复制阶段抛出异常,用于原依赖的始状态没有改变。不过,这不会影响数据结构的整体,以及用户提供类型的属性,所以你可以放心的将问题交给用户处理。 167 | 168 | 在本节开始时,我提到查询表的一个*可有可无*(nice-to-have)的特性,会将选择当前状态的快照,例如,一个`std::map<>`。这将要求锁住整个容器,用来保证拷贝副本的状态是可以索引的,这将要求锁住所有的桶。因为对于查询表的“普通”的操作,需要在同一时间获取一个桶上的一个锁,而这个操作将要求查询表将所有桶都锁住。因此,只要每次以相同的顺序进行上锁(例如,递增桶的索引值),就不会产生死锁。实现如下所示: 169 | 170 | 清单6.12 获取整个threadsafe_lookup_table作为一个`std::map<>` 171 | 172 | ``` 173 | std::map threadsafe_lookup_table::get_map() const 174 | { 175 | std::vector > locks; 176 | for(unsigned i=0;i(buckets[i].mutex)); 180 | } 181 | std::map res; 182 | for(unsigned i=0;i 227 | class threadsafe_list 228 | { 229 | struct node // 1 230 | { 231 | std::mutex m; 232 | std::shared_ptr data; 233 | std::unique_ptr next; 234 | node(): // 2 235 | next() 236 | {} 237 | 238 | node(T const& value): // 3 239 | data(std::make_shared(value)) 240 | {} 241 | }; 242 | 243 | node head; 244 | 245 | public: 246 | threadsafe_list() 247 | {} 248 | 249 | ~threadsafe_list() 250 | { 251 | remove_if([](node const&){return true;}); 252 | } 253 | 254 | threadsafe_list(threadsafe_list const& other)=delete; 255 | threadsafe_list& operator=(threadsafe_list const& other)=delete; 256 | 257 | void push_front(T const& value) 258 | { 259 | std::unique_ptr new_node(new node(value)); // 4 260 | std::lock_guard lk(head.m); 261 | new_node->next=std::move(head.next); // 5 262 | head.next=std::move(new_node); // 6 263 | } 264 | 265 | template 266 | void for_each(Function f) // 7 267 | { 268 | node* current=&head; 269 | std::unique_lock lk(head.m); // 8 270 | while(node* const next=current->next.get()) // 9 271 | { 272 | std::unique_lock next_lk(next->m); // 10 273 | lk.unlock(); // 11 274 | f(*next->data); // 12 275 | current=next; 276 | lk=std::move(next_lk); // 13 277 | } 278 | } 279 | 280 | template 281 | std::shared_ptr find_first_if(Predicate p) // 14 282 | { 283 | node* current=&head; 284 | std::unique_lock lk(head.m); 285 | while(node* const next=current->next.get()) 286 | { 287 | std::unique_lock next_lk(next->m); 288 | lk.unlock(); 289 | if(p(*next->data)) // 15 290 | { 291 | return next->data; // 16 292 | } 293 | current=next; 294 | lk=std::move(next_lk); 295 | } 296 | return std::shared_ptr(); 297 | } 298 | 299 | template 300 | void remove_if(Predicate p) // 17 301 | { 302 | node* current=&head; 303 | std::unique_lock lk(head.m); 304 | while(node* const next=current->next.get()) 305 | { 306 | std::unique_lock next_lk(next->m); 307 | if(p(*next->data)) // 18 308 | { 309 | std::unique_ptr old_next=std::move(current->next); 310 | current->next=std::move(next->next); 311 | next_lk.unlock(); 312 | } // 20 313 | else 314 | { 315 | lk.unlock(); // 21 316 | current=next; 317 | lk=std::move(next_lk); 318 | } 319 | } 320 | } 321 | }; 322 | ``` 323 | 324 | 清单6.13中的threadsafe_list<>是一个单链表,可从node的结构①中看出。一个默认构造的node,作为链表的head,其next指针②指向的是NULL。新节点都是被push_front()函数添加进去的;构造第一个新节点④,其将会在堆上分配内存③来对数据进行存储,同时将next指针置为NULL。然后,你需要获取head节点的互斥锁,为了让设置next的值⑤,也就是插入节点到列表的头部,让头节点的head.next指向这个新节点⑥。目前,还没有什么问题:你只需要锁住一个互斥量,就能将一个新的数据添加进入链表,所以这里不存在死锁的问题。同样,(缓慢的)内存分配操作在锁的范围外,所以锁能保护需要更新的一对指针。那么,现在来看一下迭代功能。 325 | 326 | 首先,来看一下for_each()⑦。这个操作需要对队列中的每个元素执行Function(函数指针);在大多数标准算法库中,都会通过传值方式来执行这个函数,这里要不就传入一个通用的函数,要不就传入一个有函数操作的类型对象。在这种情况下,这个函数必须接受类型为T的值作为参数。在链表中,会有一个“手递手”的上锁过程。在这个过程开始时,你需要锁住head及节点⑧的互斥量。然后,安全的获取指向下一个节点的指针(使用get()获取,这是因为你对这个指针没有所有权)。当指针不为NULL⑨,为了继续对数据进行处理,就需要对指向的节点进行上锁⑩。当你已经锁住了那个节点,就可以对上一个节点进行释放了⑪,并且调用指定函数⑫。当函数执行完成时,你就可以更新当前指针所指向的节点(刚刚处理过的节点),并且将所有权从next_lk移动移动到lk⑬。因为for_each传递的每个数据都是能被Function接受的,所以当需要的时,需要拷贝到另一个容器的时,或其他情况时,你都可以考虑使用这种方式更新每个元素。如果函数的行为没什么问题,这种方式是完全安全的,因为在获取节点互斥锁时,已经获取锁的节点正在被函数所处理。 327 | 328 | find_first_if()⑭和for_each()很相似;最大的区别在于find_first_if支持函数(谓词)在匹配的时候返回true,在不匹配的时候返回false⑮。当条件匹配,只需要返回找到的数据⑯,而非继续查找。你可以使用for_each()来做这件事,不过在找到之后,继续做查找就是没有意义的了。 329 | 330 | remove_if()⑰就有些不同了,因为这个函数会改变链表;所以,你就不能使用for_each()来实现这个功能。当函数(谓词)返回true⑱,对应元素将会移除,并且更新current->next⑲。当这些都做完,你就可以释放next指向节点的锁。当`std::unique_ptr`的移动超出链表范围⑳,这个节点将被删除。这种情况下,你就不需要更新当前节点了,因为你只需要修改next所指向的下一个节点就可以。当函数(谓词)返回false,那么移动的操作就和之前一样了(21)。 331 | 332 | 那么,所有的互斥量中会有死锁或条件竞争吗?答案无疑是“否”,这里要看提供的函数(谓词)是否有良好的行为。迭代通常都是使用一种方式,都是从head节点开始,并且在释放当前节点锁之前,将下一个节点的互斥量锁住,所以这里就不可能会有不同线程有不同的上锁顺序。唯一可能出现条件竞争的地方就是在remove_if()⑳中删除已有节点的时候。因为,这个操作在解锁互斥量后进行(其导致的未定义行为,可对已上锁的互斥量进行破坏)。不过,在考虑一阵后,可以确定这的确是安全的,因为你还持有前一个节点(当前节点)的互斥锁,所以不会有新的线程尝试去获取你正在删除的那个节点的互斥锁。 333 | 334 | 这里并发概率有多大呢?细粒度锁要比单锁的并发概率大很多,那我们已经获得了吗?是的,你已经获取了:同一时间内,不同线程可以在不同节点上工作,无论是其使用for_each()对每一个节点进行处理,使用find_first_if()对数据进行查找,还是使用remove_if()删除一些元素。不过,因为互斥量必须按顺序上锁,那么线程就不能交叉进行工作。当一个线程耗费大量的时间对一个特殊节点进行处理,那么其他线程就必须等待这个处理完成。在完成后,其他线程才能到达这个节点。 -------------------------------------------------------------------------------- /content/chapter6/6.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 6.4 本章总结 2 | 3 | 本章开篇,我们讨论了设计并发数据结构的意义,以及给出了一些指导意见。然后,通过设计一些通用的数据结构(栈,队列,哈希表和单链表),探究了在指导意见在实现这些数据结构的应用,并使用锁来保护数据和避免数据竞争。那么现在,你应该回看一下本章实现的那些数据结构,再回顾一下如何增加并发访问的几率,和哪里会存在潜在条件竞争。 4 | 5 | 在第7章中,我们将看一下如何避免锁完全锁定,使用底层原子操作来提供必要访问顺序约束,并给出一些指导意见。 -------------------------------------------------------------------------------- /content/chapter7/7.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第7章 无锁并发数据结构设计 2 | 3 | **本章主要内容** 4 | 5 | - 设计无锁并发数据结构
6 | - 无锁结构中内存管理技术
7 | - 对无锁数据结构设计的简单指导
8 | 9 | 上一章中,我们了解了在设计并发数据结构时会遇到的问题,根据指导意见指引,确定设计的安全性。对一些通用数据结构进行检查,并查看使用互斥锁对共享数据进行保护的实现例子。第一组例子就是使用单个互斥量来保护整个数据结构,但之后的例子就会使用多个锁来保护数据结构的不同部分,并且允许对数据结构进行更高级别的并发访问。 10 | 11 | 互斥量是一个强大的工具,其可以保证在多线程情况下可以安全的访问数据结构,并且不会有条件竞争或破坏不变量的情况存在。对于使用互斥量的代码,其原因也是很简单的:就是让互斥量来保护数据。不过,这并不会如你所想的那样;你可以回看一下第3章,回顾一下死锁形成的原因,再回顾一下基于锁的队列和查询表的例子,看一下细粒度锁是如何影响并发的。如果你能写出一个无锁并发安全的数据结构,那么就能避免这些问题。 12 | 13 | 在本章中,我们还会使用原子操作(第5章介绍)的“内存序”特性,并使用这个特性来构建无锁数据结构。设计这样的数据结构时,要格外的小心,因为这样的数据机构不是那么容易正确实现的,并且让其失败的条件很难复现。我们将从无锁数据的定义开始;而后,将继续通过几个例子来了解使用无锁数据结构的意义,最后给出一些通用的指导意见。 -------------------------------------------------------------------------------- /content/chapter7/7.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 7.1 定义和意义 2 | 3 | 使用互斥量、条件变量,以及“期望”来同步阻塞数据的算法和数据结构。应用调用库函数,将会挂起一个执行线程,直到其他线程完成某个特定的动作。库函数将调用阻塞操作来对线程进行阻塞,在阻塞移除前,线程无法继续自己的任务。通常,操作系统会完全挂起一个阻塞线程(并将其时间片交给其他线程),直到其被其他线程“解阻塞”;“解阻塞”的方式很多,比如解锁一个互斥锁、通知条件变量达成,或让“期望”就绪。 4 | 5 | 不使用阻塞库的数据结构和算法,被称为无阻塞结构。不过,无阻塞的数据结构并非都是无锁的,那么就让我们见识一下各种各样的无阻塞数据结构吧! 6 | 7 | ## 7.1.1 非阻塞数据结构 8 | 9 | 在第5章中,我们使用`std::atomic_flag`实现了一个简单的自旋锁。一起回顾一下这段代码。 10 | 11 | 清单7.1 使用`std::atomic_flag`实现了一个简单的自旋锁 12 | 13 | ``` 14 | class spinlock_mutex 15 | { 16 | std::atomic_flag flag; 17 | public: 18 | spinlock_mutex(): 19 | flag(ATOMIC_FLAG_INIT) 20 | {} 21 | void lock() 22 | { 23 | while(flag.test_and_set(std::memory_order_acquire)); 24 | } 25 | void unlock() 26 | { 27 | flag.clear(std::memory_order_release); 28 | } 29 | }; 30 | ``` 31 | 32 | 这段代码没有调用任何阻塞函数,lock()只是让循环持续调用test_and_set(),并返回false。这就是为什么取名为“自旋锁”的原因——代码“自旋”于循环当中。所以,这里没有阻塞调用,任意代码使用互斥量来保护共享数据都是非阻塞的。不过,自旋锁并不是无锁结构。这里用了一个锁,并且一次能锁住一个线程。让我们来看一下无锁结构的具体定义,这将有助于你判断哪些类型的数据结构是无锁的。 33 | 34 | ## 7.1.2 无锁数据结构 35 | 36 | 作为无锁结构,就意味着线程可以并发的访问这个数据结构。线程不能做相同的操作;一个无锁队列可能允许一个线程进行压入数据,另一个线程弹出数据,当有两个线程同时尝试添加元素时,这个数据结构将被破坏。不仅如此,当其中一个访问线程被调度器中途挂起时,其他线程必须能够继续完成自己的工作,而无需等待挂起线程。 37 | 38 | 具有“比较/交换”操作的数据结构,通常在“比较/交换”实现中都有一个循环。使用“比较/交换”操作的原因:当有其他线程同时对指定数据的修改时,代码将尝试恢复数据。当其他线程被挂起时,“比较/交换”操作执行成功,那么这样的代码就是无锁的。当执行失败时,就需要一个自旋锁了,且这个结构就是“非阻塞-有锁”的结构。 39 | 40 | 无锁算法中的循环会让一些线程处于“饥饿”状态。如有线程在“错误”时间执行,那么第一个线程将会不停得尝试自己所要完成的操作(其他程序继续执行)。“无锁-无等待”数据结构,就为了避免这种问题存在的。 41 | 42 | ## 7.1.3 无等待数据结构 43 | 44 | 无等待数据结构就是:首先,是无锁数据结构;并且,每个线程都能在有限的步数内完成操作,暂且不管其他线程是如何工作的。由于会和别的线程产生冲突,所以算法可以进行无数次尝试,因此并不是无等待的。 45 | 46 | 正确实现一个无锁的结构是十分困难的。因为,要保证每一个线程都能在有限步骤里完成操作,就需要保证每一个操作可以被一次性执行完成;当有线程执行某个操作时,不会让其他线程的操作失败。这就会让算法中所使用到的操作变的相当复杂。 47 | 48 | 考虑到获取无锁或无等待的数据结构所有权都很困难,那么就有理由来写一个数据结构了;需要保证的是,所要得获益要大于实现成本。那么,就先来找一下实现成本和所得获益的平衡点吧! 49 | 50 | ## 7.1.4 无锁数据结构的利与弊 51 | 52 | 使用无锁结构的主要原因:将并发最大化。使用基于锁的容器,会让线程阻塞或等待;互斥锁削弱了结构的并发性。在无锁数据结构中,某些线程可以逐步执行。在无等待数据结构中,无论其他线程当时在做什么,每一个线程都可以转发进度。这种理想的方式实现起来很难。结构太简单,反而不容易写,因为其就是一个自旋锁。 53 | 54 | 使用无锁数据结构的第二个原因就是鲁棒性。当一个线程在获取一个锁时被杀死,那么数据结构将被永久性的破坏。不过,当线程在无锁数据结构上执行操作,在执行到一半死亡时,数据结构上的数据没有丢失(除了线程本身的数据),其他线程依旧可以正常执行。 55 | 56 | 另一方面,当不能限制访问数据结构的线程数量时,就需要注意不变量的状态,或选择替代品来保持不变量的状态。同时,还需要注意操作的顺序约束。为了避免未定义行为,及相关的数据竞争,就必须使用原子操作对修改操作进行限制。不过,仅使用原子操作时不够的;需要确定被其他线程看到的修改,是遵循正确的顺序。 57 | 58 | 因为,没有任何锁(有可能存在活锁),死锁问题不会困扰无锁数据结构。活锁的产生是,两个线程同时尝试修改数据结构,但每个线程所做的修改操作都会让另一个线程重启,所以两个线程就会陷入循环,多次的尝试完成自己的操作。试想有两个人要过独木桥,当两个人从两头向中间走的时候,他们会在中间碰到,然后不得不再走回出发的地方,再次尝试过独木桥。这里,要打破僵局,除非有人先到独木桥的另一端(或是商量好了,或是走的快,或纯粹是运气),要不这个循环将一直重复下去。不过活锁的存在时间并不久,因为其依赖于线程调度。所以其只是对性能有所消耗,而不是一个长期的问题;但这个问题仍需要关注。根据定义,无等待的代码不会被活锁所困扰,因其操作执行步骤是有上限的。换个角度,无等待的算法要比等待算法的复杂度高,且即使没有其他线程访问数据结构,也可能需要更多步骤来完成对应操作。 59 | 60 | 这就是“无锁-无等待”代码的缺点:虽然提高了并发访问的能力,减少了单个线程的等待时间,但是其可能会将整体性能拉低。首先,原子操作的无锁代码要慢于无原子操作的代码,原子操作就相当于无锁数据结构中的锁。不仅如此,硬件必须通过同一个原子变量对线程间的数据进行同步。在第8章,你将看到与“乒乓”缓存相关的原子变量(多个线程访问同时进行访问),将会成为一个明显的性能瓶颈。在提交代码之前,无论是基于锁的数据结构,还是无锁的数据结构,对性能的检查是很重要的(最坏的等待时间,平均等待时间,整体执行时间,或者其他指标)。 61 | 62 | 让我们先来看几个例子。 -------------------------------------------------------------------------------- /content/chapter7/7.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 7.3 对于设计无锁数据结构的指导建议 2 | 3 | 本章中的例子中,看到了一些复杂的代码可让无锁结构工作正常。如果要设计自己的数据结构,一些指导建议可以帮助你找到设计重点。第6章中关于并发通用指导建议还适用,不过这里需要更多的建议。我从例子中抽取了几个实用的指导建议,在你设计无锁结构数据的时候就可以直接引用。 4 | 5 | ## 7.3.1 指导建议:使用`std::memory_order_seq_cst`的原型 6 | 7 | `std::memory_order_seq_cst`比起其他内存序要简单的多,因为所有操作都将其作为总序。本章的所有例子,都是从`std::memory_order_seq_cst`开始,只有当基本操作正常工作的时候,才放宽内存序的选择。在这种情况下,使用其他内存序就是进行优化(早起可以不用这样做)。通常,当你看整套代码对数据结构的操作后,才能决定是否要放宽该操作的内存序选择。所以,尝试放宽选择,可能会让你轻松一些。在测试后的时候,工作的代码可能会很复杂(不过,不能完全保证内存序正确)。除非你有一个算法检查器,可以系统的测试,线程能看到的所有可能性组合,这样就能保证指定内存序的正确性(这样的测试的确存在),仅是执行实现代码是远远不够的。 8 | 9 | ## 7.3.2 指导建议:对无锁内存的回收策略 10 | 11 | 这里与无锁代码最大的区别就是内存管理。当有其他线程对节点进行访问的时候,节点无法被任一线程删除;为避免过多的内存使用,还是希望这个节点在能删除的时候尽快删除。本章中介绍了三种技术来保证内存可以被安全的回收: 12 | 13 | - 等待无线程对数据结构进行访问时,删除所有等待删除的对象。 14 | 15 | - 使用风险指针来标识正在被线程访问的对象。 16 | 17 | - 对对象进行引用计数,当没有线程对对象进行引用时,将其删除。 18 | 19 | 在所有例子中,主要的想法都是使用一种方式去跟踪指定对象上的线程访问数量,当没有现成对对象进行引用的时候,将对象删除。当然,在无锁数据结构中,还有很多方式可以用来回收内存。例如,理想情况下使用一个垃圾收集器。比起算法来说,其实现更容易一些。只需要让回收器知道,当节点没被引用的时候,回收节点,就可以了。 20 | 21 | 其他替代方案就是循环使用节点,只在数据结构被销毁的时候才将节点完全删除。因为节点能被复用,那么就不会有非法的内存,所以这就能避免未定义行为的发生。这种方式的缺点:产生“ABA问题”。 22 | 23 | ## 7.3.3 指导建议:小心[ABA问题](https://en.wikipedia.org/wiki/ABA_problem) 24 | 25 | 在“基于比较/交换”的算法中要格外小心“ABA问题”。其流程是: 26 | 27 | 1. 线程1读取原子变量x,并且发现其值是A。 28 | 2. 线程1对这个值进行一些操作,比如,解引用(当其是一个指针的时候),或做查询,或其他操作。 29 | 3. 操作系统将线程1挂起。 30 | 4. 其他线程对x执行一些操作,并且将其值改为B。 31 | 5. 另一个线程对A相关的数据进行修改(线程1持有),让其不再合法。可能会在释放指针指向的内存时,代码产生剧烈的反应(大问题);或者只是修改了相关值而已(小问题)。 32 | 6. 再来一个线程将x的值改回为A。如果A是一个指针,那么其可能指向一个新的对象,只是与旧对象共享同一个地址而已。 33 | 7. 线程1继续运行,并且对x执行“比较/交换”操作,将A进行对比。这里,“比较/交换”成功(因为其值还是A),不过这是一个*错误的A*(the wrong A value)。从第2步中读取的数据不再合法,但是线程1无法言明这个问题,并且之后的操作将会损坏数据结构。 34 | 35 | 本章提到的算法不存在这个问题,不过在无锁的算法中,这个问题很常见。解决这个问题的一般方法是,让变量x中包含一个ABA计数器。“比较/交换”会对加入计数器的x进行操作。每次的值都不一样,计数随之增长,所以在x还是原值的前提下,即使有线程对x进行修改,“比较/交换”还是会失败。 36 | 37 | “ABA问题”在使用释放链表和循环使用节点的算法中很是普遍,而将节点返回给分配器,则不会引起这个问题。 38 | 39 | ## 7.3.4 指导建议:识别忙等待循环和帮助其他线程 40 | 41 | 在最终队列的例子中,已经见识到线程在执行push操作时,必须等待另一个push操作流程的完成。等待线程就会被孤立,将会陷入到忙等待循环中,当线程尝试失败的时候,会继续循环,这样就会浪费CPU的计算周期。当忙等待循环结束时,就像一个阻塞操作解除,和使用互斥锁的行为一样。通过对算法的修改,当之前的线程还没有完成操作前,让等待线程执行未完成的步骤,就能让忙等待的线程不再被阻塞。在队列例中,需要将一个数据成员转换为一个原子变量,而不是使用非原子变量和使用“比较/交换”操作来做这件事;要是在更加复杂的数据结构中,这将需要更加多的变化来满足需求。 -------------------------------------------------------------------------------- /content/chapter7/7.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 7.4 本章总结 2 | 3 | 从第6章中的基于锁的数据结构起,本章简要的描述了一些无锁数据结构的实现(通过实现栈和队列)。在这个过程中,需要小心使用原子操作的内存序,为了保证无数据竞争,以及让每个线程看到一个预制相关的数据结构。也能了解到,在无锁结构中对内存的管理是越来越难。还有,如何通过帮助线程的方式,来避免忙等待循环。 4 | 5 | 设计无锁数据结构是一项很困难的任务,并且很容易犯错;不过,这样的数据结构在某些重要情况下可对其性能进行扩展。但愿,通过本章的的一些例子,以及一些指导意见,可以帮助你设计出自己的无锁数据结构,或是实现一份研究报告中的数据结构,或用以发现离职同事代码中的bug。 6 | 7 | 不管在线程间共享怎么样的数据,你需要考虑数据结构如何使用,并且怎么样在线程间同步数据。通过设计并发访问的数据结构,就能对数据结构的功能进行封装,其他部分的代码就着重于对数据的执行,而非数据的同步。你将会在第8章中看到类似的行为:将并发数据结构转为一般的并发代码。并行算法是使用多线程的方式提高性能,因为算法需要工作线程共享它们的数据,所以对并发数据结构的选择就很关键了。 -------------------------------------------------------------------------------- /content/chapter8/8.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第8章 并发代码设计 2 | 3 | **本章主要内容** 4 | 5 | - 线程间划分数据的技术
6 | - 影响并发代码性能的因素
7 | - 性能因素是如何影响数据结构的设计
8 | - 多线程代码中的异常安全
9 | - 可扩展性
10 | - 并行算法的实现
11 | 12 | 之前章节着重于介绍使用`C++`11中的新工具来写并发代码。在第6、7章中我们了解到,如何使用这些工具来设计可并发访问的基本数据结构。这就好比一个木匠,其不仅要知道如何做一个合页,一个组合柜,或一个桌子;并发的代码的使用,要比使用/设计基本数据结构频繁的多。要将眼界放宽,就需要构建更大的结构,进行高效的工作。我将使用多线程化的`C++`标准库算法作为例子,不过这里的原则也适用于对其他应用程序的扩展。 13 | 14 | 认真思考如何进行并发化设计,对于每个编程项目来说都很重要。不过,写多线程代码的时候,需要考虑的因素比写序列化代码多得多。不仅包括一般性因素,例如:封装,耦合和聚合(这些在很多软件设计书籍中有很详细的介绍),还要考虑哪些数据需要共享,如何同步访问数据,哪些线程需要等待哪些线程,等等。 15 | 16 | 本章将会关注这些问题,从高层(但也是基本的)考虑,如何使用线程,哪些代码应该在哪些线程上执行;以及,这将如何影响代码的清晰度,并从底层细节上了解,如何构建共享数据来优化性能。 17 | 18 | 那么就先来看一下,如何在线程间划分工作。 -------------------------------------------------------------------------------- /content/chapter8/8.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 8.1 线程间划分工作的技术 2 | 3 | 试想,你被要求负责建造一座房子。为了完成任务,你需要挖地基、砌墙、添加水暖、接入电线,等等。理论上,如果你很擅长建造屋子,那么这些事情都可以由你来完成,但是这样就要花费很长很长时间,并且需要不断的切换任务。或者,你可以雇佣一些人来帮助你完成房子的建造。那么现在你需要决定雇多少人,以及雇佣人员具有什么样的技能。比如,你可以雇几个人,这几个人什么都会。现在你还得不断的切换任务,不过因为雇佣了很多人,就要比之前的速度快很多。 4 | 5 | 或者,你可以雇佣一个包工队(专家组),由瓦工,木匠,电工和水管工组成。你的包工队员只做其擅长的,所以当没有水暖任务时,水管工会坐在那里休息,喝茶或咖啡。因为人多的缘故,要比之前一个人的速度快很多,并且水管工在收拾厕所的时候,电工可以将电线连接到厨房,不过当没有属于自己的任务时,有人就会休息。即使有人在休息,你可能还是能感觉到包工队要比雇佣一群什么都会的人快。包工队不需要更换工具,并且每个人的任务都要比会的人做的快。是快还是慢,取决于特定的情况——需要尝试,进行观察。 6 | 7 | 即使雇佣包工队,你依旧可以选择人数不同的团队(可能在一个团队中,瓦工的数量超过电工)。同样,这会是一种补足,并且在建造不止一座房子的时候,会改变整体效率。即使水管工没有太多的任务,在建造过一次房子后,你依旧能让他总是处于忙碌的状态。当包工队无事可做的时候,你是不会给他们钱的;即使每次工作只有那么几个人工作,你还需要负担整个团队的开销。 8 | 9 | 建造例子已经足够说明问题;这与线程所做的事情有什么关系呢?好吧,这些问题也会发生在线程上。你需要决定使用多少个线程,并且这些线程应该去做什么。还需要决定是使用“全能”的线程去完成所有的任务,还是使用“专业”线程只去完成一件事情,或将两种方法混合。使用并发的时候,需要作出诸多选择来驱动并发,这里的选择会决定代码的性能和清晰度。因此,这里的选择至关重要,所以在你设计应用程序的结构时,再作出适当的决定。在本节中,将看到很多划分任务的技术,就先从线程间划分数据开始吧! 10 | 11 | ## 8.1.1 在线程处理前对数据进行划分 12 | 13 | 最简单的并行算法,就是并行化的`std::for_each`,其会对一个数据集中每个元素执行同一个操作。为了并行化该算法,可以为数据集中每个元素分配一个处理线程。如何划分才能获得最佳的性能,很大程度上取决于数据结构实现的细节,在之后有关性能问题的章节会再提及此问题。 14 | 15 | 最简单的分配方式:第一组N个元素分配一个线程,下一组N个元素再分配一个线程,以此类推,如图8.1所示。不管数据怎么分,每个线程都会对分配给它的元素进行操作,不过并不会和其他线程进行沟通,直到处理完成。 16 | 17 | ![](../../images/chapter8/8-1.png) 18 | 19 | 图8.1 向线程分发连续的数据块 20 | 21 | 使用过*MPI*(Message Passing Interface)[1]和OpenMP[2]的人对这个结构一定很熟悉:一项任务被分割成多个,放入一个并行任务集中,执行线程独立的执行这些任务,结果在会有主线程中合并。这种方式在2.4节中的accumulate的例子中使用过了;在这个例子中,所有并行任务和主线程的任务都是累加和。对于for_each来说,主线程将无事可做,因为这个计算不需要最终处理。 22 | 23 | 最后一步对于并行程序来说十分重要;如清单2.8中那样原始的实现,最后一步就是一个串行的。不过,这一步同样也是能被并行化的;accumulate实际上是一个递减操作,所以清单2.8中,当线程数量大于一个线程上最小处理项时,可以对accumulate进行递归调用。或者,工作线程就像做一个完整的任务一样,对步骤进行递减,而非每次都产生新的线程。 24 | 25 | 虽然这个技术十分强大,但是并不是哪都适用。有时不能像之前那样,对任务进行整齐的划分,因为只有对数据进行处理后,才能进行明确的划分。这里特别适用了递归算法,就像快速排序;下面就来看看这种特别的方式。 26 | 27 | ## 8.1.2 递归划分 28 | 29 | 快速排序有两个最基本的步骤:将数据划分到中枢元素之前或之后,然后对中枢元素之前和之后的两半数组再次进行快速排序。这里不能通过对数据的简单划分达到并行,因为,只有在一次排序结束后,才能知道哪些项在中枢元素之前和之后。当要对这种算法进行并行化,很自然的会想到使用递归。每一级的递归都会多次调用quick_sort函数,因为需要知道哪些元素在中枢元素之前和之后。递归调用是完全独立的,因为其访问的是不同的数据集,并且每次迭代都能并发执行。图8.2展示了这样的递归划分。 30 | 31 | ![](../../images/chapter8/8-2.png) 32 | 33 | 图 8.2 递归划分数据 34 | 35 | 在第4章中,已经见过这种实现。比起对大于和小于的数据块递归调用函数,使用`std::async()`可以为每一级生成小于数据块的异步任务。使用`std::async()`时,`C++`线程库就能决定何时让一个新线程执行任务,以及同步执行任务。 36 | 37 | 重要的是:对一个很大的数据集进行排序时,当每层递归都产生一个新线程,最后就会产生大量的线程。你会看到其对性能的影响,如果有太多的线程存在,那么你的应用将会运行的很慢。如果数据集过于庞大,会将线程耗尽。那么在递归的基础上进行任务的划分,就是一个不错的主意;你只需要将一定数量的数据打包后,交给线程即可。`std::async()`可以出里这种简单的情况,不过这不是唯一的选择。 38 | 39 | 另一种选择是使用`std::thread::hardware_concurrency()`函数来确定线程的数量,就像在清单2.8中的并行版accumulate()一样。然后,你可以将已排序的数据推到线程安全的栈上(如第6、7章中提及的栈)。当线程无所事事,不是已经完成对自己数据块的梳理,就是在等待一组排序数据的产生;线程可以从栈上获取这组数据,并且对其排序。 40 | 41 | 下面的代码就是使用以上方式进行的实现。 42 | 43 | 清单8.1 使用栈的并行快速排序算法——等待数据块排序 44 | 45 | ``` 46 | template 47 | struct sorter // 1 48 | { 49 | struct chunk_to_sort 50 | { 51 | std::list data; 52 | std::promise > promise; 53 | }; 54 | 55 | thread_safe_stack chunks; // 2 56 | std::vector threads; // 3 57 | unsigned const max_thread_count; 58 | std::atomic end_of_data; 59 | 60 | sorter(): 61 | max_thread_count(std::thread::hardware_concurrency()-1), 62 | end_of_data(false) 63 | {} 64 | 65 | ~sorter() // 4 66 | { 67 | end_of_data=true; // 5 68 | 69 | for(unsigned i=0;i chunk=chunks.pop(); // 7 78 | if(chunk) 79 | { 80 | sort_chunk(chunk); // 8 81 | } 82 | } 83 | 84 | std::list do_sort(std::list& chunk_data) // 9 85 | { 86 | if(chunk_data.empty()) 87 | { 88 | return chunk_data; 89 | } 90 | 91 | std::list result; 92 | result.splice(result.begin(),chunk_data,chunk_data.begin()); 93 | T const& partition_val=*result.begin(); 94 | 95 | typename std::list::iterator divide_point= // 10 96 | std::partition(chunk_data.begin(),chunk_data.end(), 97 | [&](T const& val){return val > new_lower= 105 | new_lower_chunk.promise.get_future(); 106 | chunks.push(std::move(new_lower_chunk)); // 11 107 | if(threads.size()::sort_thread,this)); 110 | } 111 | 112 | std::list new_higher(do_sort(chunk_data)); 113 | 114 | result.splice(result.end(),new_higher); 115 | while(new_lower.wait_for(std::chrono::seconds(0)) != 116 | std::future_status::ready) // 13 117 | { 118 | try_sort_chunk(); // 14 119 | } 120 | 121 | result.splice(result.begin(),new_lower.get()); 122 | return result; 123 | } 124 | 125 | void sort_chunk(boost::shared_ptr const& chunk) 126 | { 127 | chunk->promise.set_value(do_sort(chunk->data)); // 15 128 | } 129 | 130 | void sort_thread() 131 | { 132 | while(!end_of_data) // 16 133 | { 134 | try_sort_chunk(); // 17 135 | std::this_thread::yield(); // 18 136 | } 137 | } 138 | }; 139 | 140 | template 141 | std::list parallel_quick_sort(std::list input) // 19 142 | { 143 | if(input.empty()) 144 | { 145 | return input; 146 | } 147 | sorter s; 148 | 149 | return s.do_sort(input); // 20 150 | } 151 | ``` 152 | 153 | 这里,parallel_quick_sort函数⑲代表了sorter类①的功能,其支持在栈上简单的存储无序数据块②,并且对线程进行设置③。do_sort成员函数⑨主要做的就是对数据进行划分⑩。相较于对每一个数据块产生一个新的线程,这次会将这些数据块推到栈上⑪;并在有备用处理器⑫的时候,产生新线程。因为小于部分的数据块可能由其他线程进行处理,那么就得等待这个线程完成⑬。为了让所有事情顺利进行(只有一个线程和其他所有线程都忙碌时),当线程处于等待状态时⑭,就让当前线程尝试处理栈上的数据。try_sort_chunk只是从栈上弹出一个数据块⑦,并且对其进行排序⑧,将结果存在promise中,让线程对已经存在于栈上的数据块进行提取⑮。 154 | 155 | 当end_of_data没有被设置时⑯,新生成的线程还在尝试从栈上获取需要排序的数据块⑰。在循环检查中,也要给其他线程机会⑱,可以从栈上取下数据块进行更多的操作。这里的实现依赖于sorter类④对线程的清理。当所有数据都已经排序完成,do_sort将会返回(即使还有工作线程在运行),所以主线程将会从parallel_quick_sort⑳中返回,在这之后会销毁sorter对象。析构函数会设置end_of_data标志⑤,以及等待所有线程完成工作⑥。标志的设置将终止线程函数内部的循环⑯。 156 | 157 | 在这个方案中,不用为spawn_task产生的无数线程所困扰,并且也不用再依赖`C++`线程库,为你选择执行线程的数量(就像`std::async()`那样)。该方案制约线程数量的值就是`std::thread::hardware_concurrency()`的值,这样就能避免任务过于频繁的切换。不过,这里还有两个问题:线程管理和线程通讯。要解决这两个问题就要增加代码的复杂程度。虽然,线程对数据项是分开处理的,不过所有对栈的访问,都可以向栈添加新的数据块,并且移出数据块以作处理。这里重度的竞争会降低性能(即使使用无锁(无阻塞)栈),原因将会在后面提到。 158 | 159 | 这个方案使用到了一个特殊的线程池——所有线程的任务都来源于一个等待链表,然后线程会去完成任务,完成任务后会再来链表提取任务。这个线程池很有问题(包括对工作链表的竞争),这个问题的解决方案将在第9章提到。关于多处理器的问题,将会在本章后面的章节中做出更为详细的介绍(详见8.2.1)。 160 | 161 | 几种划分方法:1,处理前划分;2,递归划分(都需要事先知道数据的长度固定),还有上面的那种划分方式。事情并非总是这样好解决;当数据是动态生成,或是通过外部输入,那么这里的办法就不适用了。在这种情况下,基于任务类型的划分方式,就要好于基于数据的划分方式。 162 | 163 | ## 8.1.3 通过任务类型划分工作 164 | 165 | 虽然为每个线程分配不同的数据块,但工作的划分(无论是之前就划分好,还是使用递归的方式划分)仍然在理论阶段,因为这里每个线程对每个数据块的操作是相同的。而另一种选择是让线程做专门的工作,也就是每个线程做不同的工作,就像水管工和电工在建造一所屋子的时候所做的不同工作那样。线程可能会对同一段数据进行操作,但它们对数据进行不同的操作。 166 | 167 | 对分工的排序,也就是从并发分离关注结果;每个线程都有不同的任务,这就意味着真正意义上的线程独立。其他线程偶尔会向特定线程交付数据,或是通过触发事件的方式来进行处理;不过总体而言,每个线程只需要关注自己所要做的事情即可。其本身就是基本良好的设计,每一段代码只对自己的部分负责。 168 | 169 | **分离关注** 170 | 171 | 当有多个任务需要持续运行一段时间,或需要及时进行处理的事件(比如,按键事件或传入网络数据),且还有其他任务正在运行时,单线程应用采用的是单职责原则处理冲突。单线程的世界中,代码会执行任务A(部分)后,再去执行任务B(部分),再检查按钮事件,再检查传入的网络包,然后在循环回去,执行任务A。这将会使得任务A复杂化,因为需要存储完成状态,以及定期从主循环中返回。如果在循环中添加了很多任务,那么程序将运行的很慢;并且用户会发现,在他/她按下按键后,很久之后才会有反应。我确定你已经在一些程序中见过这种情况:你给程序分配一项任务后,发现接口会封锁,直到这项任务完成。 172 | 173 | 当使用独立线程执行任务时,操作系统会帮你处理接口问题。在执行任务A时,线程可以专注于执行任务,而不用为保存状态从主循环中返回。操作系统会自动保存状态,当需要的时候,将线程切换到任务B或任务C。如果目标系统是带有多核或多个处理器,任务A和任务B可很可能真正的并发执行。这样处理按键时间或网络包的代码,就能及时执行了。所有事情都完成的很好,用户得到了及时的响应;当然,作为开发者只需要写具体操作的代码即可,不用再将控制分支和使用用户交互混在一起了。 174 | 175 | 听起来不错,玫瑰色的愿景呀。事实真像上面所说的那样简单?一切取决于细节。如果每件事都是独立的,那么线程间就不需要交互,这样的话一切都很简单了。不幸的是,现实没那么美好。后台那些优雅的任务,经常会被用户要求做一些事情,并且它们需要通过更新用户接口的方式,来让用户知道它们完成了任务。或者,用户可能想要取消任务,这就需要用户向接口发送一条消息,告知后台任务停止运行。这两种情况都需要认真考虑,设计,以及适当的同步,不过这里担心的部分还是分离的。用户接口线程只能处理用户接口,当其他线程告诉该线程要做什么时,用户接口线程会进行更新。同样,后台线程只运行它们所关注的任务;只是,有时会发生“允许任务被其他线程所停止”的情况。在这两种情况下,后台线程需要照顾来自其他线程的请求,线程本身只知道它们请求与自己的任务有所关联。 176 | 177 | 多线程下有两个危险需要分离关注。第一个是对错误担忧的分离,主要表现为线程间共享着很多的数据,或者不同的线程要相互等待;这两种情况都是因为线程间很密切的交互。当这种情况发生,就需要看一下为什么需要这么多交互。当所有交互都有关于同样的问题,就应该使用单线程来解决,并将引用同一原因的线程提取出来。或者,当有两个线程需要频繁的交流,且没有其他线程时,那么就可以将这两个线程合为一个线程。 178 | 179 | 当通过任务类型对线程间的任务进行划分时,不应该让线程处于完全隔离的状态。当多个输入数据集需要使用同样的操作序列,可以将序列中的操作分成多个阶段,来让每个线程执行。 180 | 181 | **划分任务序列** 182 | 183 | 当任务会应用到相同操作序列,去处理独立的数据项时,就可以使用*流水线*(pipeline)系统进行并发。这好比一个物理管道:数据流从管道一端进入,在进行一系列操作后,从管道另一端出去。 184 | 185 | 使用这种方式划分工作,可以为流水线中的每一阶段操作创建一个独立线程。当一个操作完成,数据元素会放在队列中,以供下一阶段的线程提取使用。这就允许第一个线程在完成对于第一个数据块的操作,并要对第二个数据块进行操作时,第二个线程可以对第一个数据块执行管线中的第二个操作。 186 | 187 | 这就是在线程间划分数据的一种替代方案(如8.1.1描述);这种方式适合于操作开始前,且对输入数据处长度不清楚的情况。例如,数据来源可能是从网络,或者可能是通过扫描文件系统来确定要处理的文件。 188 | 189 | 流水线对于队列中耗时的操作处理的也很合理;通过对线程间任务的划分,就能对应用的性能所有改善。假设有20个数据项,需要在四核的机器上处理,并且每一个数据项需要四个步骤来完成操作,每一步都需要3秒来完成。如果你将数据分给了四个线程,那么每个线程上就有5个数据项要处理。假设在处理的时候,没有其他线程对处理过程进行影响,在12秒后4个数据项处理完成,24秒后8个数据项处理完成,以此类推。当20个数据项都完成操作,就需要1分钟的时间。在管线中就会完全不同。四步可以交给四个内核。那么现在,第一个数据项可以被每一个核进行处理,所以其还是会消耗12秒。的确,在12秒后你就能得到一个处理过的数据项,这相较于数据划分并没有好多少。不过,当流水线流动起来,事情就会不一样了;在第一个核处理第一个数据项后,数据项就会交给下一个内核,所以第一个核在处理完第一个数据项后,其还可以对第二个数据项进行处理。那么在12秒后,每3秒将会得到一个已处理的数据项,这就要好于每隔12秒完成4个数据项。 190 | 191 | 为什么整批处理的时间要长于流水线呢?因为你需要在最终核开始处理第一个元素前等待9秒。更平滑的操作,能在某些情况下获益更多。考虑如下情况:当一个系统用来播放高清数字视频。为了让视频能够播放,你至少要保证25帧每秒的解码速度。同样的,这些图像需要有均匀的间隔,才会给观众留有连续播放的感觉;一个应用可以在1秒解码100帧,不过在解完就需要暂停1s的时候,这个应用就没有意义了。另一方面,观众能接受在视频开始播放的时候有一定的延迟。这种情况,并行使用流水线就能得到稳定的解码率。 192 | 193 | 看了这么多线程间划分工作的技术,接下来让我们来看一下在多线程系统中有哪些因素会影响性能,并且这些因素是如何影响你选择划分方案的。 194 | 195 | ---------- 196 | 197 | [1] http://www.mpi-forum.org/ 198 | 199 | [2] http://www.openmp.org/ 200 | -------------------------------------------------------------------------------- /content/chapter8/8.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 8.2 影响并发代码性能的因素 2 | 3 | `多处理系统中,使用并发的方式来提高代码的效率时,你需要了解一下有哪些因素会影响并发的效率。即使已经使用多线程对关注进行分离,还需要确定是否会对性能造成负面影响。因为,在16核机器上应用的速度与单核机器相当时,用户是不会打死你的。 4 | 5 | 之后你会看到,在多线程代码中有很多因素会影响性能——对线程处理的数据做一些简单的改动(其他不变),都可能对性能产生戏剧性的效果。所以,多言无益,让我们来看一下这些因素吧,从明显的开始:目标系统有多少个处理器? 6 | 7 | ## 8.2.1 有多少个处理器? 8 | 9 | 处理器个数是影响多线程应用的首要因素。在某些情况下,你对目标硬件会很熟悉,并且针对硬件进行设计,并在目标系统或副本上进行测量。如果是这样,那你很幸运;不过,要知道这些都是很奢侈的。你可能在一个类似的平台上进行开发,不过你所使用的平台与目标平台的差异很大。例如,你可能会在一个双芯或四芯的系统上做开发,不过你的用户系统可能就只有一个处理器(可能有很多芯),或多个单芯处理器,亦或是多核多芯的处理器。在不同的平台上,并发程序的行为和性能特点就可能完全不同,所以你需要仔细考虑那些地方会被影响到,如果会被影响,就需要在不同平台上进行测试。 10 | 11 | 一个单核16芯的处理器和四核双芯或十六核单芯的处理器相同:在任何系统上,都能运行16个并发线程。当线程数量少于16个时,会有处理器处于空闲状态(除非系统同时需要运行其他应用,不过我们暂时忽略这种可能性)。另一方面,当多于16个线程在运行的时候(都没有阻塞或等待),应用将会浪费处理器的运算时间在线程间进行切换,如第1章所述。这种情况发生时,我们称其为*超额认购*(oversubscription)。 12 | 13 | 为了扩展应用线程的数量,与硬件所支持的并发线程数量一致,`C++`标准线程库提供了`std::thread::hardware_concurrency()`。使用这个函数就能知道在给定硬件上可以扩展的线程数量了。 14 | 15 | 需要谨慎使用`std::thread::hardware_concurrency()`,因为代码不会考虑有其他运行在系统上的线程(除非已经将系统信息进行共享)。最坏的情况就是,多线程同时调用`std::thread::hardware_concurrency()`函数来对线程数量进行扩展,这样将导致庞大的超额认购。`std::async()`就能避免这个问题,因为标准库会对所有的调用进行适当的安排。同样,谨慎的使用线程池也可以避免这个问题。 16 | 17 | 不过,即使你已经考虑到所有在应用中运行的线程,程序还要被同时运行的其他程序所影响。虽然,在单用户系统中,使用多个CPU密集型应用程序很罕见,但在某些领域,这种情况就很常见了。虽然系统能提供选择线程数量的机制,但这种机制已经超出`C++`标准的范围。这里的一种选择是使用与`std::async()`类似的工具,来为所有执行异步任务的线程的数量做考虑;另一种选择就是,限制每个应用使用的处理芯个数。我倒是希望,这种限制能反映到`std::thread::hardware_concurrency()`上面(不能保证)。如果你需要处理这种情况,可以看一下你所使用的系统说明,了解一下是否有相关选项可供使用。 18 | 19 | 理想算法可能会取决于问题规模与处理单元的比值。大规模并行系统中有很多的处理单元,算法可能就会同时执行很多操作,让应用更快的结束;这就要快于执行较少操作的平台,因为该平台上的每一个处理器只能执行很少的操作。 20 | 21 | 随着处理器数量的增加,另一个问题就会来影响性能:多个处理器尝试访问同一个数据。 22 | 23 | ## 8.2.2 数据争用与乒乓缓存 24 | 25 | 当两个线程并发的在不同处理器上执行,并且对同一数据进行读取,通常不会出现问题;因为数据将会拷贝到每个线程的缓存中,并且可以让两个处理器同时进行处理。不过,当有线程对数据进行修改的时候,这个修改需要更新到其他核芯的缓存中去,就要耗费一定的时间。根据线程的操作性质,以及使用到的内存序,这样的修改可能会让第二个处理器停下来,等待硬件内存更新缓存中的数据。即便是精确的时间取决于硬件的物理结构,不过根据CPU指令,这是一个特别特别慢的操作,相当于执行成百上千个独立指令。 26 | 27 | 思考下面简短的代码段: 28 | 29 | ``` 30 | std::atomic counter(0); 31 | void processing_loop() 32 | { 33 | while(counter.fetch_add(1,std::memory_order_relaxed)<100000000) 34 | { 35 | do_something(); 36 | } 37 | } 38 | ``` 39 | 40 | counter变量是全局的,所以任何线程都能调用processing_loop()去修改同一个变量。因此,当新增加的处理器时,counter变量必须要在缓存内做一份拷贝,再改变自己的值,或其他线程以发布的方式对缓存中的拷贝副本进行更新。即使用`std::memory_order_relaxed`,编译器不会为任何数据做同步操作,fetch_add是一个“读-改-写”操作,因此就要对最新的值进行检索。如果另一个线程在另一个处理器上执行同样的代码,counter的数据需要在两个处理器之间进行传递,那么这两个处理器的缓存中间就存有counter的最新值(当counter的值增加时)。如果do_something()足够短,或有很多处理器来对这段代码进行处理时,处理器将会互相等待;一个处理器准备更新这个值,另一个处理器正在修改这个值,所以该处理器就不得不等待第二个处理器更新完成,并且完成更新传递时,才能执行更新。这种情况被称为*高竞争*(high contention)。如果处理器很少需要互相等待,那么这种情况就是*低竞争*(low contention)。 41 | 42 | 在这个循环中,counter的数据将在每个缓存中传递若干次。这就叫做*乒乓缓存*(cache ping-pong),这种情况会对应用的性能有着重大的影响。当一个处理器因为等待缓存转移而停止运行时,这个处理器就不能做任何事情,所以对于整个应用来说,这就是一个坏消息。 43 | 44 | 你可能会想,这种情况不会发生在你身上;因为,你没有使用任何循环。你确定吗?那么互斥锁呢?如果你需要在循环中放置一个互斥量,那么你的代码就和之前从数据访问的差不多了。为了锁住互斥量,另一个线程必须将数据进行转移,就能弥补处理器的互斥性,并且对数据进行修改。当这个过程完成时,将会再次对互斥量进行修改,并对线程进行解锁,之后互斥数据将会传递到下一个需要互斥量的线程上去。转移时间,就是第二个线程等待第一个线程释放互斥量的时间: 45 | 46 | ``` 47 | std::mutex m; 48 | my_data data; 49 | void processing_loop_with_mutex() 50 | { 51 | while(true) 52 | { 53 | std::lock_guard lk(m); 54 | if(done_processing(data)) break; 55 | } 56 | } 57 | ``` 58 | 59 | 接下来看看最糟糕的部分:数据和互斥量已经准备好让多个线程进访问之后,当系统中的核心数和处理器数量增加时,很可能看到高竞争,以及一个处理器等待其他处理器的情况。如果在多线程情况下,能更快的对同样级别的数据进行处理,线程就会对数据和互斥量进行竞争。这里有很多这样的情况,很多线程会同时尝试对互斥量进行获取,或者同时访问变量,等等。 60 | 61 | 互斥量的竞争通常不同于原子操作的竞争,最简单的原因是,互斥量通常使用操作系统级别的序列化线程,而非处理器级别的。如果有足够的线程去执行任务,当有线程在等待互斥量时,操作系统会安排其他线程来执行任务,而处理器只会在其他线程运行在目标处理器上时,让该处理器停止工作。不过,对互斥量的竞争,将会影响这些线程的性能;毕竟,只能让一个线程在同一时间运行。 62 | 63 | 回顾第3章,一个很少更新的数据结构可以被一个“单作者,多读者”互斥量(详见3.3.2)。乒乓缓存效应可以抵消互斥所带来的收益(工作量不利时),因为所有线程访问数据(即使是读者线程)都会对互斥量进行修改。随着处理器对数据的访问次数增加,对于互斥量的竞争就会增加,并且持有互斥量的缓存行将会在核芯中进行转移,因此会增加不良的锁获取和释放次数。有一些方法可以改善这个问题,其本质就是让互斥量对多行缓存进行保护,不过这样的互斥量需要自己去实现。 64 | 65 | 如果乒乓缓存是一个糟糕的现象,那么该怎么避免它呢?在本章后面,答案会与提高并发潜能的指导意见相结合:减少两个线程对同一个内存位置的竞争。 66 | 67 | 虽然,要实现起来并不简单。即使给定内存位置被一个线程所访问,可能还是会有乒乓缓存的存在,是因为另一种叫做*伪共享*(false sharing)的效应。 68 | 69 | ## 8.2.3 伪共享 70 | 71 | 处理器缓存通常不会用来处理在单个存储位置,但其会用来处理称为*缓存行*(cache lines)的内存块。内存块通常大小为32或64字节,实际大小需要由正在使用着的处理器模型来决定。因为硬件缓存进处理缓存行大小的内存块,较小的数据项就在同一内存行的相邻内存位置上。有时,这样的设定还是挺不错:当线程访问的一组数据是在同一数据行中,对于应用的性能来说就要好于向多个缓存行进行传播。不过,当在同一缓存行存储的是无关数据,且需要被不同线程访问,这就会造成性能问题。 72 | 73 | 假设你有一个int类型的数组,并且有一组线程可以访问数组中的元素,且对数组的访问很频繁(包括更新)。通常int类型的大小要小于一个缓存行,同一个缓存行中可以存储多个数据项。因此,即使每个线程都能对数据中的成员进行访问,硬件缓存还是会产生乒乓缓存。每当线程访问0号数据项,并对其值进行更新时,缓存行的所有权就需要转移给执行该线程的处理器,这仅是为了让更新1号数据项的线程获取1号线程的所有权。缓存行是共享的(即使没有数据存在),因此使用*伪共享*来称呼这种方式。这个问题的解决办法就是对数据进行构造,让同一线程访问的数据项存在临近的内存中(就像是放在同一缓存行中),这样那些能被独立线程访问的数据将分布在相距很远的地方,并且可能是存储在不同的缓存行中。在本章接下来的内容中看到,这种思路对代码和数据设计的影响。 74 | 75 | 如果多线程访问同一内存行是一种糟糕的情况,那么在单线程下的内存布局将会如何带来哪些影响呢? 76 | 77 | ## 8.2.4 如何让数据紧凑? 78 | 79 | 伪共享发生的原因:某个线程所要访问的数据过于接近另一线程的数据,另一个是与数据布局相关的陷阱会直接影响单线程的性能。问题在于数据过于接近:当数据能被单线程访问时,那么数据就已经在内存中展开,就像是分布在不同的缓存行上。另一方面,当内存中有能被单线程访问紧凑的数据时,就如同数据分布在同一缓存行上。因此,当数据已传播,那么将会有更多的缓存行将会从处理器的缓存上加载数据,这会增加访问内存的延迟,以及降低数据的系能(与紧凑的数据存储地址相比较)。 80 | 81 | 同样的,如果数据已传播,在给定缓存行上就即包含于当前线程有关和无关的数据。在极端情况下,当有更多的数据存在于缓存中,你会对数据投以更多的关注,而非这些数据去做了什么。这就会浪费宝贵的缓存空间,增加处理器缓存缺失的情况,即使这个数据项曾经在缓存中存在过,还需要从主存中添加对应数据项到缓存中,因为在缓存中其位置已经被其他数据所占有。 82 | 83 | 现在,对于单线程代码来说就很关键了,何至于此呢?原因就是*任务切换*(task switching)。如果系统中的线程数量要比核芯多,每个核上都要运行多个线程。这就会增加缓存的压力,为了避免伪共享,努力让不同线程访问不同缓存行。因此,当处理器切换线程的时候,就要对不同内存行上的数据进行重新加载(当不同线程使用的数据跨越了多个缓存行时),而非对缓存中的数据保持原样(当线程中的数据都在同一缓存行时)。 84 | 85 | 如果线程数量多于内核或处理器数量,操作系统可能也会选择将一个线程安排给这个核芯一段时间,之后再安排给另一个核芯一段时间。因此就需要将缓存行从一个内核上,转移到另一个内核上;这样的话,就需要转移很多缓存行,也就意味着要耗费很多时间。虽然,操作系统通常避免这样的情况发生,不过当其发生的时候,对性能就会有很大的影响。 86 | 87 | 当有超级多的线程准备运行时(非等待状态),任务切换问题就会频繁发生。这个问题我们之前也接触过:超额认购。 88 | 89 | ## 8.2.5 超额认购和频繁的任务切换 90 | 91 | 多线程系统中,通常线程的数量要多于处理的数量。不过,线程经常会花费时间来等待外部I/O完成,或被互斥量阻塞,或等待条件变量,等等;所以等待不是问题。应用使用额外的线程来完成有用的工作,而非让线程在处理器处以闲置状态时继续等待。 92 | 93 | 这也并非长久之计,如果有很多额外线程,就会有很多线程准备执行,而且数量远远大于可用处理器的数量,不过操作系统就会忙于在任务间切换,以确保每个任务都有时间运行。如第1章所见,这将增加切换任务的时间开销,和缓存问题造成同一结果。当无限制的产生新线程,超额认购就会加剧,如第4章的递归快速排序那样;或者在通过任务类型对任务进行划分的时候,线程数量大于处理器数量,这里对性能影响的主要来源是CPU的能力,而非I/O。 94 | 95 | 如果只是简单的通过数据划分生成多个线程,那可以限定工作线程的数量,如8.1.2节中那样。如果超额认购是对工作的天然划分而产生,那么不同的划分方式对这种问题就没有太多益处了。之前的情况是,需要选择一个合适的划分方案,可能需要对目标平台有着更加详细的了解,不过这也只限于性能已经无法接受,或是某种划分方式已经无法提高性能的时候。 96 | 97 | 其他因素也会影响多线程代码的性能。即使CPU类型和时钟周期相同,乒乓缓存的开销可以让程序在两个单核处理器和在一个双核处理器上,产生巨大的性能差,不过这只是那些对性能影响可见的因素。接下来,让我们看一下这些因素如何影响代码与数据结构的设计。 -------------------------------------------------------------------------------- /content/chapter8/8.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 8.3 为多线程性能设计数据结构 2 | 3 | 8.1节中,我们看到了各种划分方法;并且在8.2节,了解了对性能影响的各种因素。如何在设计数据结构的时候,使用这些信息提高多线程代码的性能?这里的问题与第6、7章中的问题不同,之前是关于如何设计能够安全、并发访问的数据结构。在8.2节中,单线程中使用的数据布局就会对性能产生巨大冲击(即使数据并未与其他线程进行共享)。 4 | 5 | 关键的是,当为多线程性能而设计数据结构的时候,需要考虑*竞争*(contention),*伪共享*(false sharing)和*数据距离*(data proximity)。这三个因素对于性能都有着重大的影响,并且你通常可以改善的是数据布局,或者将赋予其他线程的数据元素进行修改。首先,让我们来看一个轻松方案:线程间划分数组元素。 6 | 7 | ## 8.3.1 为复杂操作划分数组元素 8 | 9 | 假设你有一些偏数学计算任务,比如,需要将两个很大的矩阵进行相乘。对于矩阵相乘来说,将第一个矩阵中的首行每个元素和第二个矩阵中首列每个元素相乘后,再相加,从而产生新矩阵中左上角的第一个元素。然后,第二行和第一列,产生新矩阵第一列上的第二个结果,第二行和第二列,产生新矩阵中第二列的第一个结果,以此类推。如图8.3所示,高亮展示的就是在新矩阵中第二行-第三列中的元素产生的过程。 10 | 11 | ![](../../images/chapter8/8-3.png) 12 | 13 | 图8.3 矩阵相乘 14 | 15 | 现在,让我们假设两个矩阵都有上千行和上千列,为了使用多线程来优化矩阵乘法。通常,非稀疏矩阵可以用一个大数组来代表,也就是第二行的元素紧随着第一行的,以此类推。为了完成矩阵乘法,这里就需要三个大数组。为了优化性能,你需要仔细考虑数据访问的模式,特别是向第三个数组中写入的方式。 16 | 17 | 线程间划分工作是有很多种方式的。假设矩阵的行或列数量大于处理器的数量,可以让每个线程计算出结果矩阵列上的元素,或是行上的元素,亦或计算一个子矩阵。 18 | 19 | 回顾一下8.2.3和8.2.4节,对于一个数组来说,访问连续的元素是最好的方式,因为这将会减少缓存的使用,并且降低伪共享的概率。如果要让每个线程处理几行,线程需要读取第一个矩阵中的每一个元素,并且读取第二个矩阵上的相关行上的数据,不过这里只需要对列的值进行写入。给定的两个矩阵是以行连续的方式存储,这就意味着当你访问第一个矩阵的第一行的前N个元素,然后是第二行的前N个元素,以此类推(N是列的数量)。其他线程会访问每行的的其他元素;很明显的,应该访问相邻的列,所以从行上读取的N个元素也是连续的,这将最大程度的降低伪共享的几率。当然,如果空间已经被N个元素所占有,且N个元素也就是每个缓存行上具体的存储元素数量,就会让伪共享的情况消失,因为线程将会对独立缓存行上的数据进行操作。 20 | 21 | 另一方面,当每个线程处理一组行,就需要读取第二个矩阵上的每一个数据,还要读取第一个矩阵中的相关行上的值,不过这里只需要对行上的值进行写入。因为矩阵是以行连续的方式存储,那么现在可以以N行的方式访问所有的元素。如果再次选择相邻行,这就意味着线程现在只能写入N行,这里就有不能被其他线程所访问的连续内存块。那么让线程对每组列进行处理就是一个改进,因为伪共享只可能有在一个内存块的最后几个元素和下一个元素的开始几个上发生,不过具体的时间还要根据目标架构来决定。 22 | 23 | 第三个选择——将矩阵分成小矩阵块?这可以看作先对列进行划分,再对行进行划分。因此,划分列的时候,同样有伪共享的问题存在。如果你可以选择内存块所拥有行的数量,就可以有效的避免伪共享;将大矩阵划分为小块,对于读取来说是有好处的:就不再需要读取整个源矩阵了。这里,只需要读取目标矩形里面相关行列的值就可以了。具体的来看,考虑1,000行和1,000列的两个矩阵相乘。就会有1百万个元素。如果有100个处理器,这样就可以每次处理10行的数据,也就是10,000个元素。不过,为了计算着10,000个元素,就需要对第二个矩阵中的全部内容进行访问(1百万个元素),再加上10,000个相关行(第一个矩阵)上的元素,大概就要访问1,010,000个元素。另外,硬件能处理100x100的数据块(总共10,000个元素),这就需要对第一个矩阵中的100行进行访问(100x1,000=100,000个元素),还有第二个矩阵中的100列(另外100,000个)。这才只有200,000个元素,就需要五轮读取才能完成。如果这里读取的元素少一些,缓存缺失的情况就会少一些,对于性能来说就好一些。 24 | 25 | 因此,将矩阵分成小块或正方形的块,要比使用单线程来处理少量的列好的多。当然,可以根据源矩阵的大小和处理器的数量,在运行时对块的大小进行调整。和之前一样,当性能是很重要的指标,就需要对目标架构上的各项指标进行测量。 26 | 27 | 如果不做矩阵乘法,该如何对上面提到的方案进行应用呢?同样的原理可以应用于任何情况,这种情况就是有很大的数据块需要在线程间进行划分;仔细观察所有数据访问的各个方面,以及确定性能问题产生的原因。各种领域中,出现问题的情况都很相似:改变划分方式就能够提高性能,而不需要对基本算法进行任何修改。 28 | 29 | OK,我们已经了解了访问数组是如何对性能产生影响的。那么其他类型的数据结构呢? 30 | 31 | ## 8.3.2 其他数据结构中的数据访问模式 32 | 33 | 根本上讲,同样的考虑适用于想要优化数据结构的数据访问模式,就像优化对数组的访问: 34 | 35 | - 尝试调整数据在线程间的分布,就能让同一线程中的数据紧密联系在一起。 36 | 37 | - 尝试减少线程上所需的数据量。 38 | 39 | - 尝试让不同线程访问不同的存储位置,以避免伪共享。 40 | 41 | 当然,应用于其他数据结构上会比较麻烦。例如,对二叉树划分就要比其他结构困难,有用与没用要取决于树的平衡性,以及需要划分的节点数量。同样,树的的属性决定了其节点会动态的进行分配,并且在不同的地方进行释放。 42 | 43 | 现在,节点在不同的地方释放倒不是一个严重的问题,不过这就意味着处理器需要在缓存中存储很多东西,这实际上是有好处的。当多线程需要旋转树的时候,就需要对树中的所有节点进行访问,不过当树中的节点只包括指向实际值的指针时,处理器只能从主存中对数据进行加载。如果数据正在被访问线程所修改,这就能避免节点数据,以及树数据结构间的伪共享。 44 | 45 | 这里就和用一个互斥量来保护数据类似了。假设你有一个简单的类,包含一些数据项和一个用于保护数据的互斥量(在多线程环境下)。如果互斥量和数据项在内存中很接近,对与一个需要获取互斥量的线程来说是很理想的情况;需要的数据可能早已存入处理器的缓存中了,因为在之前为了对互斥量进行修改,已经加载了需要的数据。不过,这还有一个缺点:当其他线程尝试锁住互斥量时(第一个线程还没有是释放),线程就能对对应的数据项进行访问。互斥锁是当做一个“读-改-写”原子操作实现的,对于相同位置的操作都需要先获取互斥量,如果互斥量已锁,那就会调用系统内核。这种“读-改-写”操作,可能会让数据存储在缓存中,让线程获取的互斥量变得毫无作用。从目前互斥量的发展来看,这并不是个问题;线程不会直到互斥量解锁,才接触互斥量。不过,当互斥量共享同一缓存行时,其中存储的是线程已使用的数据,这时拥有互斥量的线程将会遭受到性能打击,因为其他线程也在尝试锁住互斥量。 46 | 47 | 一种测试伪共享问题的方法是:对大量的数据块填充数据,让不同线程并发的进行访问。比如,你可以使用: 48 | 49 | ``` 50 | struct protected_data 51 | { 52 | std::mutex m; 53 | char padding[65536]; // 65536字节已经超过一个缓存行的数量级 54 | my_data data_to_protect; 55 | }; 56 | ``` 57 | 58 | 用来测试互斥量竞争或 59 | 60 | ``` 61 | struct my_data 62 | { 63 | data_item1 d1; 64 | data_item2 d2; 65 | char padding[65536]; 66 | }; 67 | my_data some_array[256]; 68 | ``` 69 | 70 | 用来测试数组数据中的伪共享。如果这样能够提高性能,你就能知道伪共享在这里的确存在。 71 | 72 | 当然,在设计并发的时候有更多的数据访问模式需要考虑,现在让我们一起来看一些附加的注意事项。 -------------------------------------------------------------------------------- /content/chapter8/8.6-chinese.md: -------------------------------------------------------------------------------- 1 | # 8.6 本章总结 2 | 3 | 本章我们讨论了很多东西。我们从划分线程间的工作开始(比如,数据提前划分或让线程形成流水线)。之后,以低层次视角来看多线程下的性能问题,顺带了解了伪共享和数据通讯;了解访问数据的模式对性能的影响。再后,了解了附加注意事项是如何影响并发代码设计的,比如:异常安全和可扩展性。最后,用一些并行算法实现来结束了本章,在设计这些并行算法实现时碰到的问题,在设计其他并行代码的时候也会遇到。 4 | 5 | 本章中,关于线程池的部分被转移了。线程池——一个预先设定的线程组,会将任务指定给池中的线程。很多不错的想法可以用来设计一个不过的线程池;所以我们将在下一章中来看一些有关线程池的问题,以及高级线程管理方式。 -------------------------------------------------------------------------------- /content/chapter9/9.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 第9章 高级线程管理 2 | 3 | **本章主要内容** 4 | 5 | - 线程池
6 | - 处理线程池中任务的依赖关系
7 | - 池中线程如何获取任务
8 | - 中断线程
9 | 10 | 之前的章节中,我们通过创建`std::thread`对象来对线程进行管理。在一些情况下,这种方式不可行了,因为需要在线程的整个生命周期中对其进行管理,并根据硬件来确定线程数量,等等。理想情况是将代码划分为最小块,再并发执行,之后交给处理器和标准库,进行性能优化。 11 | 12 | 另一种情况是,当使用多线程来解决某个问题时,在某个条件达成的时候,可以提前结束。可能是因为结果已经确定,或者因为产生错误,亦或是用户执行终止操作。无论是哪种原因,线程都需要发送“请停止”请求,放弃任务,清理,然后尽快停止。 13 | 14 | 本章,我们将了解一下管理线程和任务的机制,从自动管理线程数量和自动管理任务划分开始。 -------------------------------------------------------------------------------- /content/chapter9/9.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 9.2 中断线程 2 | 3 | 很多情况下,使用信号来终止一个长时间运行的线程是合理的。这种线程的存在,可能是因为工作线程所在的线程池被销毁,或是用户显式的取消了这个任务,亦或其他各种原因。不管是什么原因,原理都一样:需要使用信号来让未结束线程停止运行。这里需要一种合适的方式让线程主动的停下来,而非让线程戛然而止。 4 | 5 | 你可能会给每种情况制定一个独立的机制,这样做的意义不大。不仅因为用统一的机制会更容易在之后的场景中实现,而且写出来的中断代码不用担心在哪里使用。C++11标准没有提供这样的机制,不过实现这样的机制也并不困难。 6 | 7 | 在了解一下应该如何实现这种机制前,先来了解一下启动和中断线程的接口。 8 | 9 | ## 9.2.1 启动和中断线程 10 | 11 | 先看一下外部接口,需要从可中断线程上获取些什么?最起码需要和`std::thread`相同的接口,还要多加一个interrupt()函数: 12 | 13 | ``` 14 | class interruptible_thread 15 | { 16 | public: 17 | template 18 | interruptible_thread(FunctionType f); 19 | void join(); 20 | void detach(); 21 | bool joinable() const; 22 | void interrupt(); 23 | }; 24 | ``` 25 | 26 | 类内部可以使用`std::thread`来管理线程,并且使用一些自定义数据结构来处理中断。现在,从线程的角度能看到什么呢?“能用这个类来中断线程”——需要一个断点(*interruption point*)。在不添加多余的数据的前提下,为了使断点能够正常使用,就需要使用一个没有参数的函数:interruption_point()。这意味着中断数据结构可以访问thread_local变量,并在线程运行时,对变量进行设置,因此当线程调用interruption_point()函数时,就会去检查当前运行线程的数据结构。我们将在后面看到interruption_point()的具体实现。 27 | 28 | thread_local标志是不能使用普通的`std::thread`管理线程的主要原因;需要使用一种方法分配出一个可访问的interruptible_thread实例,就像新启动一个线程一样。在使用已提供函数来做这件事情前,需要将interruptible_thread实例传递给`std::thread`的构造函数,创建一个能够执行的线程,就像下面的代码清单所实现。 29 | 30 | 清单9.9 interruptible_thread的基本实现 31 | 32 | ``` 33 | class interrupt_flag 34 | { 35 | public: 36 | void set(); 37 | bool is_set() const; 38 | }; 39 | thread_local interrupt_flag this_thread_interrupt_flag; // 1 40 | 41 | class interruptible_thread 42 | { 43 | std::thread internal_thread; 44 | interrupt_flag* flag; 45 | public: 46 | template 47 | interruptible_thread(FunctionType f) 48 | { 49 | std::promise p; // 2 50 | internal_thread=std::thread([f,&p]{ // 3 51 | p.set_value(&this_thread_interrupt_flag); 52 | f(); // 4 53 | }); 54 | flag=p.get_future().get(); // 5 55 | } 56 | void interrupt() 57 | { 58 | if(flag) 59 | { 60 | flag->set(); // 6 61 | } 62 | } 63 | }; 64 | ``` 65 | 66 | 提供函数f是包装了一个lambda函数③,线程将会持有f副本和本地promise变量(p)的引用②。在新线程中,lambda函数设置promise变量的值到this_thread_interrupt_flag(在thread_local①中声明)的地址中,为的是让线程能够调用提供函数的副本④。调用线程会等待与其future相关的promise就绪,并且将结果存入到flag成员变量中⑤。注意,即使lambda函数在新线程上执行,对本地变量p进行悬空引用,都没有问题,因为在新线程返回之前,interruptible_thread构造函数会等待变量p,直到变量p不被引用。实现没有考虑处理汇入线程,或分离线程。所以,需要flag变量在线程退出或分离前已经声明,这样就能避免悬空问题。 67 | 68 | interrupt()函数相对简单:需要一个线程去做中断时,需要一个合法指针作为一个中断标志,所以可以仅对标志进行设置⑥。 69 | 70 | ## 9.2.2 检查线程是否中断 71 | 72 | 现在就可以设置中断标志了,不过不检查线程是否被中断,这样的意义就不大了。使用interruption_point()函数最简单的情况;可以在一个安全的地方调用这个函数,如果标志已经设置,就可以抛出一个thread_interrupted异常: 73 | 74 | ``` 75 | void interruption_point() 76 | { 77 | if(this_thread_interrupt_flag.is_set()) 78 | { 79 | throw thread_interrupted(); 80 | } 81 | } 82 | ``` 83 | 84 | 代码中可以在适当的地方使用这个函数: 85 | 86 | ``` 87 | void foo() 88 | { 89 | while(!done) 90 | { 91 | interruption_point(); 92 | process_next_item(); 93 | } 94 | } 95 | ``` 96 | 97 | 虽然也能工作,但不理想。最好实在线程等待或阻塞的时候中断线程,因为这时的线程不能运行,也就不能调用interruption_point()函数!在线程等待的时候,什么方式才能去中断线程呢? 98 | 99 | ## 9.2.3 中断等待——条件变量 100 | 101 | OK,需要仔细选择中断的位置,并通过显式调用interruption_point()进行中断,不过在线程阻塞等待的时候,这种办法就显得苍白无力了,例如:等待条件变量的通知。就需要一个新函数——interruptible_wait()——就可以运行各种需要等待的任务,并且可以知道如何中断等待。之前提到,可能会等待一个条件变量,所以就从它开始:如何做才能中断一个等待的条件变量呢?最简单的方式是,当设置中断标志时,需要提醒条件变量,并在等待后立即设置断点。为了让其工作,需要提醒所有等待对应条件变量的线程,就能确保感谢兴趣的线程能够苏醒。伪苏醒是无论如何都要处理的,所以其他线程(非感兴趣线程)将会被当作伪苏醒处理——两者之间没什么区别。interrupt_flag结构需要存储一个指针指向一个条件变量,所以用set()函数对其进行提醒。为条件变量实现的interruptible_wait()可能会看起来像下面清单中所示。 102 | 103 | 清单9.10 为`std::condition_variable`实现的interruptible_wait有问题版 104 | 105 | ``` 106 | void interruptible_wait(std::condition_variable& cv, 107 | std::unique_lock& lk) 108 | { 109 | interruption_point(); 110 | this_thread_interrupt_flag.set_condition_variable(cv); // 1 111 | cv.wait(lk); // 2 112 | this_thread_interrupt_flag.clear_condition_variable(); // 3 113 | interruption_point(); 114 | } 115 | ``` 116 | 117 | 假设函数能够设置和清除相关条件变量上的中断标志,代码会检查中断,通过interrupt_flag为当前线程关联条件变量①,等待条件变量②,清理相关条件变量③,并且再次检查中断。如果线程在等待期间被条件变量所中断,中断线程将广播条件变量,并唤醒等待该条件变量的线程,所以这里就可以检查中断。不幸的是,代码有两个问题。第一个问题比较明显,如果想要线程安全:`std::condition_variable::wait()`可以抛出异常,所以这里会直接退出,而没有通过条件变量删除相关的中断标志。这个问题很容易修复,就是在析构函数中添加相关删除操作即可。 118 | 119 | 第二个问题就不大明显了,这段代码存在条件竞争。虽然,线程可以通过调用interruption_point()被中断,不过在调用wait()后,条件变量和相关中断标志就没有什么系了,因为线程不是等待状态,所以不能通过条件变量的方式唤醒。就需要确保线程不会在最后一次中断检查和调用wait()间被唤醒。这里,不对`std::condition_variable`的内部结构进行研究;不过,可通过一种方法来解决这个问题:使用lk上的互斥量对线程进行保护,这就需要将lk传递到set_condition_variable()函数中去。不幸的是,这将产生两个新问题:需要传递一个互斥量的引用到一个不知道生命周期的线程中去(这个线程做中断操作)为该线程上锁(调用interrupt()的时候)。这里可能会死锁,并且可能访问到一个已经销毁的互斥量,所以这种方法不可取。当不能完全确定能中断条件变量等待——没有interruptible_wait()情况下也可以时(可能有些严格),那有没有其他选择呢?一个选择就是放置超时等待,使用wait_for()并带有一个简单的超时量(比如,1ms)。在线程被中断前,算是给了线程一个等待的上限(以时钟刻度为基准)。如果这样做了,等待线程将会看到更多因为超时而“伪”苏醒的线程,不过超时也不轻易的就帮助到我们。与interrupt_flag相关的实现的一个实现放在下面的清单中展示。 120 | 121 | 清单9.11 为`std::condition_variable`在interruptible_wait中使用超时 122 | 123 | ``` 124 | class interrupt_flag 125 | { 126 | std::atomic flag; 127 | std::condition_variable* thread_cond; 128 | std::mutex set_clear_mutex; 129 | 130 | public: 131 | interrupt_flag(): 132 | thread_cond(0) 133 | {} 134 | 135 | void set() 136 | { 137 | flag.store(true,std::memory_order_relaxed); 138 | std::lock_guard lk(set_clear_mutex); 139 | if(thread_cond) 140 | { 141 | thread_cond->notify_all(); 142 | } 143 | } 144 | 145 | bool is_set() const 146 | { 147 | return flag.load(std::memory_order_relaxed); 148 | } 149 | 150 | void set_condition_variable(std::condition_variable& cv) 151 | { 152 | std::lock_guard lk(set_clear_mutex); 153 | thread_cond=&cv; 154 | } 155 | 156 | void clear_condition_variable() 157 | { 158 | std::lock_guard lk(set_clear_mutex); 159 | thread_cond=0; 160 | } 161 | 162 | struct clear_cv_on_destruct 163 | { 164 | ~clear_cv_on_destruct() 165 | { 166 | this_thread_interrupt_flag.clear_condition_variable(); 167 | } 168 | }; 169 | }; 170 | 171 | void interruptible_wait(std::condition_variable& cv, 172 | std::unique_lock& lk) 173 | { 174 | interruption_point(); 175 | this_thread_interrupt_flag.set_condition_variable(cv); 176 | interrupt_flag::clear_cv_on_destruct guard; 177 | interruption_point(); 178 | cv.wait_for(lk,std::chrono::milliseconds(1)); 179 | interruption_point(); 180 | } 181 | ``` 182 | 183 | 如果有谓词(相关函数)进行等待,1ms的超时将会完全在谓词循环中完全隐藏: 184 | 185 | ``` 186 | template 187 | void interruptible_wait(std::condition_variable& cv, 188 | std::unique_lock& lk, 189 | Predicate pred) 190 | { 191 | interruption_point(); 192 | this_thread_interrupt_flag.set_condition_variable(cv); 193 | interrupt_flag::clear_cv_on_destruct guard; 194 | while(!this_thread_interrupt_flag.is_set() && !pred()) 195 | { 196 | cv.wait_for(lk,std::chrono::milliseconds(1)); 197 | } 198 | interruption_point(); 199 | } 200 | ``` 201 | 202 | 这会让谓词被检查的次数增加许多,不过对于简单调用wait()这套实现还是很好用的。超时变量很容易实现:通过制定时间,比如:1ms或更短。OK,对于`std::condition_variable`的等待,就需要小心应对了;`std::condition_variable_any`呢?还是能做的更好吗? 203 | 204 | ## 9.2.4 使用`std::condition_variable_any`中断等待 205 | 206 | `std::condition_variable_any`与`std::condition_variable`的不同在于,`std::condition_variable_any`可以使用任意类型的锁,而不仅有`std::unique_lock`。可以让事情做起来更加简单,并且`std::condition_variable_any`可以比`std::condition_variable`做的更好。因为能与任意类型的锁一起工作,就可以设计自己的锁,上锁/解锁interrupt_flag的内部互斥量set_clear_mutex,并且锁也支持等待调用,就像下面的代码。 207 | 208 | 清单9.12 为`std::condition_variable_any`设计的interruptible_wait 209 | 210 | ``` 211 | class interrupt_flag 212 | { 213 | std::atomic flag; 214 | std::condition_variable* thread_cond; 215 | std::condition_variable_any* thread_cond_any; 216 | std::mutex set_clear_mutex; 217 | 218 | public: 219 | interrupt_flag(): 220 | thread_cond(0),thread_cond_any(0) 221 | {} 222 | 223 | void set() 224 | { 225 | flag.store(true,std::memory_order_relaxed); 226 | std::lock_guard lk(set_clear_mutex); 227 | if(thread_cond) 228 | { 229 | thread_cond->notify_all(); 230 | } 231 | else if(thread_cond_any) 232 | { 233 | thread_cond_any->notify_all(); 234 | } 235 | } 236 | 237 | template 238 | void wait(std::condition_variable_any& cv,Lockable& lk) 239 | { 240 | struct custom_lock 241 | { 242 | interrupt_flag* self; 243 | Lockable& lk; 244 | 245 | custom_lock(interrupt_flag* self_, 246 | std::condition_variable_any& cond, 247 | Lockable& lk_): 248 | self(self_),lk(lk_) 249 | { 250 | self->set_clear_mutex.lock(); // 1 251 | self->thread_cond_any=&cond; // 2 252 | } 253 | 254 | void unlock() // 3 255 | { 256 | lk.unlock(); 257 | self->set_clear_mutex.unlock(); 258 | } 259 | 260 | void lock() 261 | { 262 | std::lock(self->set_clear_mutex,lk); // 4 263 | } 264 | 265 | ~custom_lock() 266 | { 267 | self->thread_cond_any=0; // 5 268 | self->set_clear_mutex.unlock(); 269 | } 270 | }; 271 | custom_lock cl(this,cv,lk); 272 | interruption_point(); 273 | cv.wait(cl); 274 | interruption_point(); 275 | } 276 | // rest as before 277 | }; 278 | 279 | template 280 | void interruptible_wait(std::condition_variable_any& cv, 281 | Lockable& lk) 282 | { 283 | this_thread_interrupt_flag.wait(cv,lk); 284 | } 285 | ``` 286 | 287 | 自定义的锁类型在构造的时候,需要所锁住内部set_clear_mutex①,对thread_cond_any指针进行设置,并引用`std::condition_variable_any`传入锁的构造函数中②。Lockable引用将会在之后进行存储,其变量必须被锁住。现在可以安心的检查中断,不用担心竞争了。如果这时中断标志已经设置,那么标志一定是在锁住set_clear_mutex时设置的。当条件变量调用自定义锁的unlock()函数中的wait()时,就会对Lockable对象和set_clear_mutex进行解锁③。这就允许线程可以尝试中断其他线程获取set_clear_mutex锁;以及在内部wait()调用之后,检查thread_cond_any指针。这就是在替换`std::condition_variable`后,所拥有的功能(不包括管理)。当wait()结束等待(因为等待,或因为伪苏醒),因为线程将会调用lock()函数,这里依旧要求锁住内部set_clear_mutex,并且锁住Lockable对象④。现在,在wait()调用时,custom_lock的析构函数中⑤清理thread_cond_any指针(同样会解锁set_clear_mutex)之前,可以再次对中断进行检查。 288 | 289 | ## 9.2.5 中断其他阻塞调用 290 | 291 | 这次轮到中断条件变量的等待了,不过其他阻塞情况,比如:互斥锁,等待future等等,该怎么办呢?通常情况下,可以使用`std::condition_variable`的超时选项,因为在实际运行中不可能很快的将条件变量的等待终止(不访问内部互斥量或future的话)。不过,在某些情况下,你知道知道你在等待什么,这样就可以让循环在interruptible_wait()函数中运行。作为一个例子,这里为`std::future<>`重载了interruptible_wait()的实现: 292 | 293 | ``` 294 | template 295 | void interruptible_wait(std::future& uf) 296 | { 297 | while(!this_thread_interrupt_flag.is_set()) 298 | { 299 | if(uf.wait_for(lk,std::chrono::milliseconds(1)== 300 | std::future_status::ready) 301 | break; 302 | } 303 | interruption_point(); 304 | } 305 | ``` 306 | 307 | 等待会在中断标志设置好的时候,或future准备就绪的时候停止,不过实现中每次等待future的时间只有1ms。这就意味着,中断请求被确定前,平均等待的时间为0.5ms(这里假设存在一个高精度的时钟)。通常wait_for至少会等待一个时钟周期,所以如果时钟周期为15ms,那么结束等待的时间将会是15ms,而不是1ms。接受与不接受这种情况,都得视情况而定。如果这必要,且时钟支持的话,可以持续削减超时时间。这种方式将会让线程苏醒很多次,来检查标志,并且增加线程切换的开销。 308 | 309 | OK,我们已经了解如何使用interruption_point()和interruptible_wait()函数检查中断。 310 | 311 | 当中断被检查出来了,要如何处理它呢? 312 | 313 | ## 9.2.6 处理中断 314 | 315 | 从中断线程的角度看,中断就是thread_interrupted异常,因此能像处理其他异常那样进行处理。 316 | 317 | 特别是使用标准catch块对其进行捕获: 318 | 319 | ``` 320 | try 321 | { 322 | do_something(); 323 | } 324 | catch(thread_interrupted&) 325 | { 326 | handle_interruption(); 327 | } 328 | ``` 329 | 330 | 捕获中断,进行处理。其他线程再次调用interrupt()时,线程将会再次被中断,这就被称为*断点*(interruption point)。如果线程执行的是一系列独立的任务,就会需要断点;中断一个任务,就意味着这个任务被丢弃,并且该线程就会执行任务列表中的其他任务。 331 | 332 | 因为thread_interrupted是一个异常,在能够被中断的代码中,之前线程安全的注意事项都是适用的,就是为了确保资源不会泄露,并在数据结构中留下对应的退出状态。通常,让线程中断是可行的,所以只需要让异常传播即可。不过,当异常传入`std::thread`的析构函数时,`std::terminate()`将会调用,并且整个程序将会终止。为了避免这种情况,需要在每个将interruptible_thread变量作为参数传入的函数中放置catch(thread_interrupted)处理块,可以将catch块包装进interrupt_flag的初始化过程中。因为异常将会终止独立进程,就能保证未处理的中断是异常安全的。interruptible_thread构造函数中对线程的初始化,实现如下: 333 | 334 | ``` 335 | internal_thread=std::thread([f,&p]{ 336 | p.set_value(&this_thread_interrupt_flag); 337 | 338 | try 339 | { 340 | f(); 341 | } 342 | catch(thread_interrupted const&) 343 | {} 344 | }); 345 | ``` 346 | 347 | 下面,我们来看个更加复杂的例子。 348 | 349 | ## 9.2.7 应用退出时中断后台任务 350 | 351 | 试想,在桌面上查找一个应用。这就需要与用户互动,应用的状态需要能在显示器上显示,就能看出应用有什么改变。为了避免影响GUI的响应时间,通常会将处理线程放在后台运行。后台进程需要一直执行,直到应用退出;后台线程会作为应用启动的一部分被启动,并且在应用终止的时候停止运行。通常这样的应用只有在机器关闭时,才会退出,因为应用需要更新应用最新的状态,就需要全时间运行。在某些情况下,当应用被关闭,需要使用有序的方式将后台线程关闭,其中一种方式就是中断。 352 | 353 | 下面清单中为一个系统实现了简单的线程管理部分。 354 | 355 | 清单9.13 在后台监视文件系统 356 | 357 | ``` 358 | std::mutex config_mutex; 359 | std::vector background_threads; 360 | 361 | void background_thread(int disk_id) 362 | { 363 | while(true) 364 | { 365 | interruption_point(); // 1 366 | fs_change fsc=get_fs_changes(disk_id); // 2 367 | if(fsc.has_changes()) 368 | { 369 | update_index(fsc); // 3 370 | } 371 | } 372 | } 373 | 374 | void start_background_processing() 375 | { 376 | background_threads.push_back( 377 | interruptible_thread(background_thread,disk_1)); 378 | background_threads.push_back( 379 | interruptible_thread(background_thread,disk_2)); 380 | } 381 | 382 | int main() 383 | { 384 | start_background_processing(); // 4 385 | process_gui_until_exit(); // 5 386 | std::unique_lock lk(config_mutex); 387 | for(unsigned i=0;i ———. U.S. Patent and Trademark Office application 20040107227, “Method for efficient implementation of dynamic lock-free data structures with safe memory reclamation.” 12 | 13 | Sutter, Herb, Exceptional C++: 47 Engineering Puzzles, Programming Problems, and Solutions (Addison Wesley Professional, 1999), ISBN 0-201-61562-2. 14 | 15 | > ———. “The Free Lunch Is Over: A Fundamental Turn Toward Concurrency in Software,” in Dr. Dobb’s Journal 30, no. 3 (March 2005). Also available at http://www.gotw.ca/publications/concurrency-ddj.htm. 16 | 17 | ## 在线资源 18 | 19 | Atomic Ptr Plus Project Home, http://atomic-ptr-plus.sourceforge.net/ 20 | 21 | Boost C++ library collection, http://www.boost.org 22 | 23 | C++0x/C++11 Support in GCC, http://gcc.gnu.org/projects/cxx0x.html 24 | 25 | C++11—The Recently Approved New ISO C++ Standard, http://www.research.att.com/~bs/C++0xFAQ.html 26 | 27 | Erlang Programming Language, http://www.erlang.org/ 28 | 29 | GNU General Public License, http://www.gnu.org/licenses/gpl.html 30 | 31 | Haskell Programming Language, http://www.haskell.org/ 32 | 33 | IBM Statement of Non-Assertion of Named Patents Against OSS, http://www.ibm.com/ibm/licensing/patents/pledgedpatents.pdf 34 | 35 | Intel Building Blocks for Open Source, http://threadingbuildingblocks.org/ 36 | 37 | The just::thread Implementation of the C++ Standard Thread Library, http://www.stdthread.co.uk 38 | 39 | Message Passing Interface Forum, http://www.mpi-forum.org/ 40 | 41 | Multithreading API for C++0X—A Layered Approach, C++ Standards Committee Paper N2094, http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2094.html 42 | 43 | OpenMP, http://www.openmp.org/ 44 | 45 | SETI@Home, http://setiathome.ssl.berkeley.edu/ -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/cover.jpg -------------------------------------------------------------------------------- /cover/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/cover/background.jpg -------------------------------------------------------------------------------- /cover/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/cover/logo.png -------------------------------------------------------------------------------- /images/chapter1/1-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter1/1-1.png -------------------------------------------------------------------------------- /images/chapter1/1-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter1/1-2.png -------------------------------------------------------------------------------- /images/chapter1/1-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter1/1-3.png -------------------------------------------------------------------------------- /images/chapter1/1-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter1/1-4.png -------------------------------------------------------------------------------- /images/chapter3/3-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter3/3-1.png -------------------------------------------------------------------------------- /images/chapter4/4-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter4/4-1.png -------------------------------------------------------------------------------- /images/chapter4/4-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter4/4-2.png -------------------------------------------------------------------------------- /images/chapter4/4-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter4/4-3.png -------------------------------------------------------------------------------- /images/chapter5/5-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-1.png -------------------------------------------------------------------------------- /images/chapter5/5-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-2.png -------------------------------------------------------------------------------- /images/chapter5/5-3-table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-3-table.png -------------------------------------------------------------------------------- /images/chapter5/5-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-3.png -------------------------------------------------------------------------------- /images/chapter5/5-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-4.png -------------------------------------------------------------------------------- /images/chapter5/5-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-5.png -------------------------------------------------------------------------------- /images/chapter5/5-6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-6.png -------------------------------------------------------------------------------- /images/chapter5/5-7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter5/5-7.png -------------------------------------------------------------------------------- /images/chapter6/6-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter6/6-1.png -------------------------------------------------------------------------------- /images/chapter7/7-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter7/7-1.png -------------------------------------------------------------------------------- /images/chapter8/8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter8/8-1.png -------------------------------------------------------------------------------- /images/chapter8/8-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter8/8-2.png -------------------------------------------------------------------------------- /images/chapter8/8-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter8/8-3.png -------------------------------------------------------------------------------- /images/chapter8/amdahl_law.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Cpp_Concurrency_In_Action/15e7c8ce2e92839942604665dcccabee5a958233/images/chapter8/amdahl_law.png --------------------------------------------------------------------------------