├── .gitignore ├── LICENSE ├── README.md ├── SUMMARY.md ├── book.json ├── content ├── Data-Structures │ ├── 10.1-chinese.md │ └── 10.2-chinese.md ├── Further-Information │ ├── 11.1-chinese.md │ ├── 11.2-chinese.md │ ├── 11.3-chinese.md │ └── 11.4-chinese.md ├── History-Quick-Overview.md ├── How-you-should-read-the-book.md ├── Pattrns │ ├── Best-Practices │ │ ├── 9.0-chinese.md │ │ ├── 9.1-chinese.md │ │ ├── 9.2-chinese.md │ │ └── 9.3-chinese.md │ ├── Concurrent-Architecture │ │ ├── 8.0-chinese.md │ │ ├── 8.1-chinese.md │ │ ├── 8.2-chinese.md │ │ └── 8.3-chinese.md │ ├── Patterns-and-Best-Practices │ │ ├── 6.0-chinese.md │ │ ├── 6.1-chinese.md │ │ ├── 6.2-chinese.md │ │ ├── 6.3-chinese.md │ │ └── 6.4-chinese.md │ └── Synchronisation-Patterns │ │ ├── 7.0-chinese.md │ │ ├── 7.1-chinese.md │ │ └── 7.2-chinese.md ├── Reader-Testimonials.md ├── Source-Code.md └── The-Details │ ├── Case-Studies │ ├── 4.0-chinese.md │ ├── 4.1-chinese.md │ ├── 4.2-chinese.md │ ├── 4.3-chinese.md │ └── 4.4-chinese.md │ ├── Memory-Model │ ├── 1.0-chinese.md │ ├── 1.1-chinese.md │ ├── 1.2-chinese.md │ ├── 1.3-chinese.md │ ├── 1.4-chinese.md │ └── 1.5-chinese.md │ ├── Multithreading │ ├── 2.0-chinese.md │ ├── 2.1-chinese.md │ ├── 2.2-chinese.md │ ├── 2.3-chinese.md │ ├── 2.4-chinese.md │ └── 2.5-chinese.md │ ├── Parallel-Algorithms-of-the-Standard │ ├── 3.0-chinese.md │ ├── 3.1-chinese.md │ ├── 3.2-chinese.md │ ├── 3.3-chinese.md │ └── 3.4-chinese.md │ └── The-Future-CPP-20-23 │ ├── 5.0-chinese.md │ ├── 5.1-chinese.md │ ├── 5.2-chinese.md │ ├── 5.3-chinese.md │ ├── 5.4-chinese.md │ ├── 5.5-chinese.md │ ├── 5.6-chinese.md │ ├── 5.7-chinese.md │ └── 5.8-chinese.md ├── cover.jpg └── images ├── Further-Information ├── Challenges │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── CppMem │ ├── 1.png │ └── 2.png └── The-Time-Library │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── History-Quick-Overview ├── 0.png ├── 1.png ├── 2.png └── 3.png ├── Patterns ├── Best-Practices │ ├── 1.png │ ├── 10.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png ├── Concurrent-Architecture │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ └── 8.png └── Synchronisation-Patterns │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── 6.png │ ├── 7.png │ ├── 8.png │ └── 9.png └── detail ├── Case-Studies ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png ├── 17.png ├── 18.png ├── 19.png ├── 20.png ├── 21.png ├── 22.png ├── 23.png ├── 24.png ├── 25.png ├── 26.png ├── 27.png ├── 28.png ├── 29.png ├── 30.png ├── 31.png ├── 32.png ├── 33.png ├── 34.png ├── 35.png ├── 36.png ├── 37.png ├── 38.png ├── 39.png ├── 40.png ├── 41.png ├── 42.png ├── 43.png ├── 44.png ├── 45.png ├── 46.png ├── 47.png ├── 48.png ├── 49.png ├── 50.png ├── 51.png ├── 52.png ├── 53.png ├── 54.png └── 55.png ├── Parallel-Algorithms-of-the-Standard ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── The-Future-CPP-20-23 ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── memory-model ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png ├── 17.png ├── 18.png ├── 19.png ├── 2.png ├── 20.png ├── 21.png ├── 22.png ├── 23.png ├── 24.png ├── 25.png ├── 26.png ├── 27.png ├── 28.png ├── 29.png ├── 3.png ├── 30.png ├── 31.png ├── 32.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png └── multithreading ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png ├── 17.png ├── 18.png ├── 19.png ├── 2.png ├── 20.png ├── 21.png ├── 22.png ├── 23.png ├── 24.png ├── 25.png ├── 26.png ├── 27.png ├── 28.png ├── 29.png ├── 3.png ├── 30.png ├── 31.png ├── 32.png ├── 33.png ├── 34.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.pdf 2 | /_book/ 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Concurrency with Modern C++ 2 | 3 | * 作者:Rainer Grimm 4 | * 译者:陈晓伟 5 | * 原文发布时间:2019年03月19日 6 | 7 | > 翻译是译者用自己的思想,换一种语言,对原作者想法的重新阐释。鉴于我的学识所限,误解和错译在所难免。如果你能买到本书的原版,且有能力阅读英文,请直接去读原文。因为与之相较,我的译文可能根本不值得一读。 8 | > 9 | >

— 云风,程序员修炼之道第2版译者

10 | 11 | ## 本书概述 12 | 13 | 每个专业的C++开发者,都应该知晓的并发性。 14 | 15 | 本书是一场关于C++并发的旅程。 16 | 17 | * C++11和C++14创建了并发和并行的基础件。 18 | 19 | * C++17中,将标准模板库(STL)的大部分算法并行化。这意味着大多数基于STL的算法可以串行、并行或向量化执行。 20 | 21 | * C++的并发之旅并没有停止。C++20/23中还有增强版future、协程([coroutines](https://en.cppreference.com/w/cpp/language/coroutines))、事件性内存([transactional_memory](https://en.cppreference.com/w/cpp/language/transactional_memory))等等。 22 | 23 | 本书解释了C++中的并发性,并提供了许多代码示例。因此,可以将理论与实践相结合。 24 | 25 | 因为这本书与并发相关,所以我展示了很多容易出错的地方,并展示避免或解决它们的方案。 26 | 27 | ## 书与作者 28 | 29 | 这本书使用英语完成。在写书之前,我在我的英文博客www.ModernesCpp.com发布了要写这本书的消息,并得到了很多人的回复。有大概有50多个人要帮我校对。特别感谢我的闺女Juliette,对本书的布局进行升华;还有我的儿子,你是本书的第一个审阅者哦。当然,还有很多很多人 : NikosAthanasiou, RobertBadea, JoeDas, Jonas Devlieghere, Randy Hormann, Lasse Natvig, Erik Newton, Ian Reeve, Bart Vandewoestyne, Dafydd Walters, Andrzej Warzynski, 以及Enrico Zschemisch。 30 | 31 | 我已经做了20多年的软件架构师、团队带头人和讲师。在业余时间,我喜欢了解关于C++、Python和Haskell的信息。2016年时,我决定为自己工作。我会组织关于C++和Python的研讨会。 32 | 33 | 在Oberstdorf时,我换了一个新的髋关节(义肢)。本书的前半部分是在诊所期间所写,这段时间充满挑战,对我写书也有很大的帮助。 34 | 35 | ## 本书相关 36 | 37 | * github翻译地址:https://github.com/xiaoweiChen/Concurrency-with-Modern-Cpp 38 | * 英文原版PDF:https://ru.b-ok2.org/book/5247958/3b69d3 -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # 目录 2 | 3 | * [读者推荐](content/Reader-Testimonials.md) 4 | * [代码说明](content/Source-Code.md) 5 | * [如何阅读](content/How-you-should-read-the-book.md) 6 | * [C++并发历史概述](content/History-Quick-Overview.md) 7 | * 详细介绍 8 | * [内存模型](content/The-Details/Memory-Model/1.0-chinese.md) 9 | * [内存模型的基础知识](content/The-Details/Memory-Model/1.1-chinese.md) 10 | * [编程协议](content/The-Details/Memory-Model/1.2-chinese.md) 11 | * [原子操作](content/The-Details/Memory-Model/1.3-chinese.md) 12 | * [同步和顺序](content/The-Details/Memory-Model/1.4-chinese.md) 13 | * [栅栏(Fences)](content/The-Details/Memory-Model/1.5-chinese.md) 14 | * [多线程](content/The-Details/Multithreading/2.0-chinese.md) 15 | * [线程](content/The-Details/Multithreading/2.1-chinese.md) 16 | * [共享数据](content/The-Details/Multithreading/2.2-chinese.md) 17 | * [线程-本地数据](content/The-Details/Multithreading/2.3-chinese.md) 18 | * [条件变量](content/The-Details/Multithreading/2.4-chinese.md) 19 | * [任务](content/The-Details/Multithreading/2.5-chinese.md) 20 | * [标准库的并行算法](content/The-Details/Parallel-Algorithms-of-the-Standard/3.0-chinese.md) 21 | * [执行策略](content/The-Details/Parallel-Algorithms-of-the-Standard/3.1-chinese.md) 22 | * [算法](content/The-Details/Parallel-Algorithms-of-the-Standard/3.2-chinese.md) 23 | * [新算法](content/The-Details/Parallel-Algorithms-of-the-Standard/3.3-chinese.md) 24 | * [性能概况](content/The-Details/Parallel-Algorithms-of-the-Standard/3.4-chinese.md) 25 | * [案例研究](content/The-Details/Case-Studies/4.0-chinese.md) 26 | * [求向量元素的加和](content/The-Details/Case-Studies/4.1-chinese.md) 27 | * [单例模式:线程安全的初始化](content/The-Details/Case-Studies/4.2-chinese.md) 28 | * [使用CppMem进行优化](content/The-Details/Case-Studies/4.3-chinese.md) 29 | * [总结](content/The-Details/Case-Studies/4.4-chinese.md) 30 | * [C++20/23的特性](content/The-Details/The-Future-CPP-20-23/5.0-chinese.md) 31 | * [关于执行](content/The-Details/The-Future-CPP-20-23/5.1-chinese.md) 32 | * [可协作中断的线程](content/The-Details/The-Future-CPP-20-23/5.2-chinese.md) 33 | * [原子智能指针](content/The-Details/The-Future-CPP-20-23/5.3-chinese.md) 34 | * [扩展特性](content/The-Details/The-Future-CPP-20-23/5.4-chinese.md) 35 | * [门闩和栅栏](content/The-Details/The-Future-CPP-20-23/5.5-chinese.md) 36 | * [协程](content/The-Details/The-Future-CPP-20-23/5.6-chinese.md) 37 | * [事务性内存](content/The-Details/The-Future-CPP-20-23/5.7-chinese.md) 38 | * [任务块](content/The-Details/The-Future-CPP-20-23/5.8-chinese.md) 39 | * 模式 40 | * [模式和最佳实践](content/Pattrns/Patterns-and-Best-Practices/6.0-chinese.md) 41 | * [相关历史](content/Pattrns/Patterns-and-Best-Practices/6.1-chinese.md) 42 | * [价值所在](content/Pattrns/Patterns-and-Best-Practices/6.2-chinese.md) 43 | * [模式与最佳实践](content/Pattrns/Patterns-and-Best-Practices/6.3-chinese.md) 44 | * [反模式](content/Pattrns/Patterns-and-Best-Practices/6.4-chinese.md) 45 | * [同步模式](content/Pattrns/Synchronisation-Patterns/7.0-chinese.md) 46 | * [处理共享](content/Pattrns/Synchronisation-Patterns/7.1-chinese.md) 47 | * [处理突变](content/Pattrns/Synchronisation-Patterns/7.2-chinese.md) 48 | * [并发架构](content/Pattrns/Concurrent-Architecture/8.0-chinese.md) 49 | * [活动对象](content/Pattrns/Concurrent-Architecture/8.1-chinese.md) 50 | * [监控对象](content/Pattrns/Concurrent-Architecture/8.2-chinese.md) 51 | * [半同步/半异步](content/Pattrns/Concurrent-Architecture/8.3-chinese.md) 52 | * [最佳实践](content/Pattrns/Best-Practices/9.0-chinese.md) 53 | * [通常情况](content/Pattrns/Best-Practices/9.1-chinese.md) 54 | * [多线程](content/Pattrns/Best-Practices/9.2-chinese.md) 55 | * [内存模型](content/Pattrns/Best-Practices/9.3-chinese.md) 56 | * 数据结构 57 | * [有锁结构](content/Data-Structures/10.1-chinese.md) 58 | * [无锁结构](content/Data-Structures/10.2-chinese.md) 59 | * 更多信息 60 | * [挑战](content/Further-Information/11.1-chinese.md) 61 | * [时间库](content/Further-Information/11.2-chinese.md) 62 | * [CppMem-概述](content/Further-Information/11.3-chinese.md) 63 | * [术语表](content/Further-Information/11.4-chinese.md) 64 | 65 | -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "《Concurrency with Modern C++》中文版", 3 | 4 | "author" : "陈晓伟 chenxiaowei", 5 | 6 | "description":"作为对《Concurrency with Modern C++》英文版的中文翻译。", 7 | 8 | "language":"zh-hans", 9 | 10 | "direction":"ltr", 11 | 12 | "pluginsConfig": { 13 | "fontSettings": { 14 | "theme": "white", 15 | "family": "msyh", 16 | "size": 2 17 | }, 18 | "plugins": [ 19 | "katex", 20 | "ace" 21 | ] 22 | }, 23 | 24 | "pdf": { 25 | "pageNumbers": false, 26 | "paperSize": "a4", 27 | "margin": { 28 | "right": 10, 29 | "left": 10, 30 | "top": 10, 31 | "bottom": 20 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /content/Data-Structures/10.1-chinese.md: -------------------------------------------------------------------------------- 1 | #有锁结构 2 | 3 | -------------------------------------------------------------------------------- /content/Data-Structures/10.2-chinese.md: -------------------------------------------------------------------------------- 1 | #无锁结构 -------------------------------------------------------------------------------- /content/Further-Information/11.3-chinese.md: -------------------------------------------------------------------------------- 1 | # CppMem-概述 2 | 3 | [CppMem](http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/)是一个交互式工具,用于对C++小代码段的内存模型行为进行研究。它应该是每个认真处理内存模型程序员的必备工具。 4 | 5 | CppMem的网上版本(也可以把它安装在你的个人电脑上)以两种方式提供服务: 6 | 7 | 1. CppMem验证小代码段的行为,基于选择的C++内存模型,该工具考虑所有可能的线程交错,将每个线程可视化到一个图中,并用附加的细节对这些图进行注释。 8 | 2. CppMem的精确分析,可以更加深入了解C++内存模型。简言之,CppMem是一个帮助理解内存模型的工具。 9 | 10 | 当然,必须跨过一些门槛,这通常是强大工具的共性。CppMem的本质是提供与这个极具挑战性的主题相关的非常详细的分析,并且是高度可配置的。因此,我才打算介绍该工具的各种组件。 11 | 12 | ## 简单概述 13 | 14 | 我对CppMem的简单概述是基于默认配置的。这篇概述只是提供了进一步的实验基础,应该有助于理解我正在进行的优化过程。 15 | 16 | ![](../../images/Further-Information/CppMem/1.png) 17 | 18 | 简单起见,我引用了屏幕截图中的红色数字。 19 | 20 | ### 1. Model模型 21 | 22 | * 指定C++内存模型。首选是C++11内存模型的一个(简化)等价的变体。 23 | 24 | ### 2. Program 程序 25 | 26 | * 包含可执行程序,其语法类似于简化的C++11。确切地说,不能直接将C或C++代码程序复制到CppMem中。 27 | * 可以在许多典型多线程场景之间进行切换。要获得这些程序的详细信息,请阅读这篇写得非常好的文章,该文章将[C++并发性数学化](http://www.cl.cam.ac.uk/~pes20/cpp/popl085ap-sewell.pdf)。当然,也可以运行自己的代码。 28 | * CppMem是关于多线程的,所以可以使用多线程的快捷方式。 29 | * 可以使用表达式`{ { {…|||…} } }`。三个点`(…)`表示每个线程的工作包。 30 | * 如果使用表达式`x.readvalue(1)`,则CppMem会计算线程交错的情况,其中线程会为`x`赋值1。 31 | 32 | ### 3. Display Relations 关系显示 33 | 34 | * 描述原子操作、栅栏和锁上的读、写和读写改之间的关系。 35 | * 可以使用复选框显式地启用带注释的图中的关系。 36 | * 有三种关系,最有趣的是原始关系和派生关系之间的粗略区别。这里使用的是默认值。 37 | * 渊源关系: 38 | * sb: sequenced-before 序前 39 | * rf: read from 读取 40 | * mo: modification order 修改顺序 41 | * sc: sequentially consistent 按顺序一致 42 | * lo: lock order 锁定顺序 43 | * 派生关系: 44 | * sw: synchronises-with 与...同步 45 | * dob: dependency-ordered-before 序前依赖 46 | * unsequenced_races: 单线程中的竞争 47 | * data_races: 线程内的数据竞争 48 | 49 | ### 4. Display Layout 布局显示 50 | 51 | * 可以选择使用哪个[Doxygraph](https://sourceforge.net/projects/doxygraph/)图形。 52 | 53 | ### 5. Model Predicates 模型谓词 54 | 55 | * 使用此按钮,可以为所选模型设置谓词,这会导致不一致(非无数据争用)的执行,所以当执行不一致,就会看到不一致执行的原因。我在这本书里不使用这个按钮。 56 | 57 | 有关更多细节,请参阅[文档](http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/help.html)。 58 | 59 | 作为对CppMem的入门,这就足够了。现在,是时候尝试一下CppMem了。 60 | 61 | CppMem提供了许多示例。 62 | 63 | ### 示例 64 | 65 | 这些示例展示了使用并发代码,特别是使用无锁代码时的典型用例。可以将这些例子,分成几类。 66 | 67 | **论文** 68 | 69 | 示例/论文类别为您提供了一些示例,这些示例在本文中对[C++并发性的数学化](https://www.cl.cam.ac.uk/~pes20/cpp/popl085ap-sewell.pdf)进行了深入的讨论。 70 | 71 | * data_race.c : x上的数据竞争 72 | * partial_sb.c : 单线程中计算的序前 73 | * unsequenced_race.c : 根据评价顺序,对x上未排序的竞争进行评价 74 | * sc_atomics.c : 正确的使用原子变量 75 | * thread_create_and_asw.c : 额外的同步——与适当的线程创建同步 76 | 77 | 让我们从第一个示例开始。 78 | 79 | **测试运行** 80 | 81 | 从CppMem样本中选择data_race.c程序。run之后,立即显示有一个数据竞争。 82 | 83 | ![](../../images/Further-Information/CppMem/2.png) 84 | 85 | 简单起见,只解释示例中的红色数字。 86 | 87 | 1. 很容易观察到的数据竞争。一个线程写`x (x==3)`,另一个线程不同步读`x (x==3)`。 88 | 2. 由于C++内存模型,两个线程可能交织在一起运行,其中只有一个与所选模型一致。如果在表达式`x==3`中的`x`,在主函数中进行赋值`int x= 2`,则会出现这种情况。图中在用`rf`和`sw`标注的边缘显示了这种关系。 89 | 3. 不同的线程交错之间切换显得非常有趣。 90 | 4. 该图显示关系中启用的所有关系。 91 | * `a:Wna x=2`在图表中是第`a`中表述,它是非原子性的。`Wna`表示“非原子写入”。 92 | * 图中的关键是`x (b:Wna)`的写和`x (C:Rna)`的读之间的连线。这也就是`x`上的数据竞争。 93 | 94 | **进一步分类** 95 | 96 | 进一步的分类会关注于无锁编程的方面。每个类别的示例都有不同的形式,每个表单使用不同的内存顺序。有关类别的更多讨论,请阅读前面提到的[将C++并发性数学化](https://www.cl.cam.ac.uk/~pes20/cpp/popl085ap-sewell.pdf)的文章。如果可能的话,我会用顺序一致性来表示程序。 97 | 98 | **存储缓冲(示例/SB_store_buffering)** 99 | 100 | 两个线程分别写入不同的位置,然后从另一个位置读取。 101 | 102 | SB+sc_sc+sc_sc+sc.c 103 | 104 | ```c++ 105 | // SB+sc_sc+sc_sc 106 | // Store Buffering (or Dekker's), with all four accesses SC atomics 107 | // Question: can the two reads both see 0 in the same execution? 108 | int main() { 109 | atomic_int x=0; atomic_int y=0; 110 | {{{ { y.store(1,memory_order_seq_cst); 111 | r1=x.load(memory_order_seq_cst); } 112 | ||| { x.store(1,memory_order_seq_cst); 113 | r2=y.load(memory_order_seq_cst); } }}} 114 | return 0; 115 | } 116 | ``` 117 | 118 | **消息传递(示例/MP_message_passing)** 119 | 120 | 一个线程写入数据(非原子变量)并设置一个原子标志,而另一个线程等待读取数据标志(非原子变量)。 121 | 122 | MP+na_sc+sc_na.c 123 | 124 | ```c++ 125 | // MP+na_sc+sc_na 126 | // Message Passing, of data held in non-atomic x, 127 | // with sc atomic stores and loads on y giving release/acquire synchronisation 128 | // Question: is the read of x required to see the new data value 1 129 | // rather than the initial state value 0? 130 | int main() { 131 | int x=0; atomic_int y=0; 132 | {{{ { x=1; 133 | y.store(1,memory_order_seq_cst); } 134 | ||| { r1=y.load(memory_order_seq_cst).readsvalue(1); 135 | r2=x; } }}} 136 | return 0; 137 | } 138 | ``` 139 | 140 | **读取缓冲(例子/LB_load_buffering)** 141 | 142 | 两个读操作可以看到之后的其他线程的写操作吗? 143 | 144 | Lb+sc_sc+sc_sc.c 145 | 146 | ```c++ 147 | // LB+sc_sc+sc_sc 148 | // Load Buffering, with all four accesses sequentially consistent atomics 149 | // Question: can the two reads both see 1 in the same execution? 150 | int main() { 151 | atomic_int x=0; atomic_int y=0; 152 | {{{ { r1=x.load(memory_order_seq_cst); 153 | y.store(1,memory_order_seq_cst); } 154 | ||| { r2=y.load(memory_order_seq_cst); 155 | x.store(1,memory_order_seq_cst); } }}} 156 | return 0; 157 | } 158 | ``` 159 | 160 | **从写到读的因果关系(例子/WRC)** 161 | 162 | 第三个线程是否看到第一个线程的写操作? 163 | 164 | * 第一个线程写x。 165 | * 第二个线程从中读取数据并写入到y。 166 | * 第三个线程读取x。 167 | 168 | WRC+rel+acq_rel+acq_rlx.c 169 | 170 | ```c++ 171 | // WRC 172 | // the question is whether the final read is required to see 1 173 | // With two release/acquire pairs, it is 174 | int main() { 175 | atomic_int x = 0; 176 | atomic_int y = 0; 177 | {{{ x.store(1,mo_release); 178 | ||| { r1=x.load(mo_acquire).readsvalue(1); 179 | y.store(1,mo_release); } 180 | ||| { r2=y.load(mo_acquire).readsvalue(1); 181 | r3=x.load(mo_relaxed); } 182 | }}} 183 | return 0; 184 | } 185 | ``` 186 | 187 | **独立读-独立写(示例\IRIW)** 188 | 189 | 两个线程写入不同的位置,第二个线程能以不同的顺序看到写操作吗? 190 | 191 | IRIW+rel+rel+acq_acq+acq_acq.c 192 | 193 | ```c++ 194 | // IRIW with release/acquire 195 | // the question is whether the reading threads have 196 | // to see the writes to x and y in the same order. 197 | // With release/acquire, they do not. 198 | int main() { 199 | atomic_int x = 0; atomic_int y = 0; 200 | {{{ x.store(1, memory_order_release); 201 | ||| y.store(1, memory_order_release); 202 | ||| { r1=x.load(memory_order_acquire).readsvalue(1); 203 | r2=y.load(memory_order_acquire).readsvalue(0); } 204 | ||| { r3=y.load(memory_order_acquire).readsvalue(1); 205 | r4=x.load(memory_order_acquire).readsvalue(0); } 206 | }}}; 207 | return 0; 208 | } 209 | ``` 210 | 211 | -------------------------------------------------------------------------------- /content/Further-Information/11.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 术语表 2 | 3 | 本术语表只为基本术语提供参考。 4 | 5 | ## ACID 6 | 7 | 事务具有原子性、一致性、隔离性和持久性(ACID)属性的操作。在C++中,除了持久性之外,事务性内存的所有属性都保持不变。 8 | 9 | * 原子性:执行或不执行块的所有语句。 10 | * 一致性:系统始终处于一致的状态,所有事务构建顺序一致。 11 | * 独立性:每个事务在完全隔离的情况下运行。 12 | * 会对事务的持久性进行记录。 13 | 14 | ## CAS 15 | 16 | CAS表示compare-and-swap,是一个原子操作。它将内存位置与给定值进行比较,如果内存位置与给定值相同,则修改内存位置的值。在C++中,CAS操作有`std::compare_exchange_strong`和`std::compare_exchange_weak`。 17 | 18 | ## 可调用单元 19 | 20 | 可调用单元的行为类似于函数。不仅是函数,还有函数对象和Lambda函数。如果一个可调用单元接受一个参数,它就被称为一元可调用单元;如果有两个参数,就是二元可调用单元。 21 | 22 | 谓词是返回布尔值的特殊可调用项。 23 | 24 | ## 并发性 25 | 26 | 并发性意味着多个任务的重叠执行。而且,并发是并行的超集。 27 | 28 | ## 临界区 29 | 30 | 临界区是一段代码,最多只有一个线程可以访问。 31 | 32 | ## 立即求值 33 | 34 | 如果立即求值,则立即求出表达式的值,则该策略与延迟求值正交。立即求值通常也称为贪婪求值。 35 | 36 | ## Executor 37 | 38 | 执行者是与特定执行上下文相关联的对象。它提供一个或多个执行函数,用于为可调用的函数对象创建执行代理。 39 | 40 | ## 函数对象 41 | 42 | 首先,不要叫它们[函子](https://en.wikipedia.org/wiki/Functor)。这是一个明确的数学术语,叫做[范畴理论](https://en.wikipedia.org/wiki/Category_theory)。 43 | 44 | 函数对象是行为类似于函数,通过实现函数调用操作符来实现这一点。由于函数对象是对象,因此可以有属性和状态。 45 | 46 | ```c++ 47 | struct Square{ 48 | void operator()(int& i){i= i*i;} 49 | }; 50 | 51 | std::vector myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 52 | 53 | std::for_each(myVec.begin(), myVec.end(), Square()); 54 | 55 | for (auto v: myVec) std::cout << v << " "; // 1 4 9 16 25 36 49 64 81 100 56 | ``` 57 | 58 | > **实例化函数对象** 59 | > 60 | > 常见的错误是在算法中使用函数对象(`Square`)的名称,而不是函数对象(`Square()`)本身的实例,比如:`std::for_each(myVec.begin(), myVec.end(), Square)`,应该使用:`std::for_each(myVec.begin(), myVec.end(), Square())`。 61 | 62 | ## Lambda函数 63 | 64 | Lambda函数可以就地提供需要的功能,编译器当场就能得到相应的信息,因此具有极佳的优化潜力。Lambda函数可以通过值或引用来接收它们的参数,还可以通过值或引用捕获已定义的变量。 65 | 66 | ```c++ 67 | std::vector myVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; 68 | std::for_each(myVec.begin(), myVec.end(), [](int& i){ i= i*i; }); 69 | // 1 4 9 16 25 36 49 64 81 100 70 | ``` 71 | 72 | > **应该首选Lambda函数** 73 | > 74 | > 如果可调用的功能是简短和可以自解释的,使用Lambda函数最好不过。Lambda函数通常比函数或函数对象更快,而且更容易理解。 75 | 76 | ## 延迟求值 77 | 78 | [延迟求值](https://en.wikipedia.org/wiki/Lazy_evaluation)的情况下,仅在需要时才对表达式求值。该策略与立即求值策略正交。延迟求值通常称为按需调用。 79 | 80 | ## 无锁 81 | 82 | 如果保证了系统范围内的进程无影响,那么非阻塞算法就是无锁的。 83 | 84 | ##未唤醒 85 | 86 | 未唤醒是指,线程由于竞争条件而丢失唤醒通知的情况。 87 | 88 | 如果使用没有使用谓词,可能会发生这种情况。 89 | 90 | ## 数学规律 91 | 92 | 某个集合X上的一个二进制操作(*): 93 | 94 | * 结合律,满足x, y, z中的所有x, y, z的结合律:(x * y) * z = x * (y * z) 95 | * 交换律,满足所有x和y的交换律x * y = y * x 96 | 97 | ## 内存位置 98 | 99 | 内存位置的详解可以参考[cppreference.com](http://en.cppreference.com/w/cpp/language/memory_model) 100 | 101 | * 标量类型的对象(算术类型、指针类型、枚举类型或`std::nullptr_t`。 102 | * 非零长度的最大连续序列。 103 | 104 | ## 内存模型 105 | 106 | 内存模型定义了对象和内存位置之间的关系,特别是处理了以下问题:如果两个线程访问相同的内存位置,会发生什么情况。 107 | 108 | ##修改顺序 109 | 110 | 对特定原子对象M的所有修改,都以特定的顺序进行,这个顺序称为M的修改顺序。因此,线程读取原子对象时,不会看到比线程已经观察到的值更“旧”的值。 111 | 112 | ## Monad(单子) 113 | 114 | Haskell作为一种纯函数语言,只有纯函数。这些纯函数的一个关键特性,当给定相同的参数时,总是返回相同的结果。有了这个[透明参照](https://en.wikipedia.org/wiki/Referential_transparency)的属性,Haskell函数才不会有副作用。因此,Haskell有一个概念上的问题。到处都是有副作用的计算,这些计算可能会失败,可能返回未知数量的结果,或者依赖于环境。为了解决这个概念上的问题,Haskell使用单子并将它们嵌入到纯函数语言中。 115 | 116 | 经典的单子封装: 117 | 118 | * I/O单子:计算输入和输出的结果。 119 | * 可能性单子:可能会返回计算结果的单子。 120 | * 错误单子:计算可能失败。 121 | * 列表单子:计算可以有任意数量的结果。 122 | * 状态单子:基于状态的计算。 123 | * 读者单子:基于环境的计算。 124 | 125 | 单子的概念来自数学中的[范畴理论](https://en.wikipedia.org/wiki/Category_theory),其处理对象之间的映射。单子是抽象的数据类型,将简单的类型转换为丰富的类型。这些丰富类型的值称为一元值。当进入单子,一个值只能由一个函数组合转换成另一个一元值。 126 | 127 | 这种组合尊重了单子的独特结构。因此,当发生错误,错误单子中断它的计算,或重新构建状态单子的状态。 128 | 129 | 一个单子包括三个部分: 130 | 131 | * 类型构造函数:定义简单数据类型,如何成为一元数据类型。 132 | * 函数: 133 | * 恒等函数:在单子中引入一个简单的值。 134 | * 绑定操作符:定义如何将函数应用于一元值,以获得新的一元值。 135 | * 功能规则: 136 | * 恒等函数的左右必须是恒等元素。 137 | * 函数的复合必须遵循结合律。 138 | 139 | 要使错误单子成为类型类单子的实例,错误单子必须支持恒等函数和绑定操作符,这两个函数定义了错误单子应该如何处理计算中的错误。如果使用错误单子,错误处理会在后台完成。 140 | 141 | 单子由两个控制流组成:用于计算结果的显式控制和用于处理特定副作用的隐式控制流。 142 | 143 | 当然,也可以用更少的词来定义单子:“单子只是内函子类中的一个独异点(monoid)。” 144 | 145 | 单子在C++中变得越来越重要。在C++ 17中,添加了[`std::optional`](http://en.cppreference.com/w/cpp/utility/optional) ,这是一种可能性单子。在C++20/23中,可能会从Eric Niebler那里得到扩展future和[范围库]( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4128.html),二者也都是单子。 146 | 147 | ## 无阻塞 148 | 149 | 如果任何线程的失败或挂起,不会导致另一个线程的失败或挂起,则称为非阻塞。这个定义来自于[《Java并发实践》]( http://jcip.net/)。 150 | 151 | ## 并行性 152 | 153 | 并行性意味着同时执行多个任务。并行性是并发性的一个子集。 154 | 155 | ## 谓词 156 | 157 | 谓词是返回布尔值的可调用单元。如果一个谓词有一个参数,它就称为一元谓词。如果一个谓词有两个参数,就称为二元谓词。 158 | 159 | ## 模式 160 | 161 | “每个模式规则都是一个由三部分组成,表明了特定上下文、问题和解决方案之间的关系。“ —— [Christopher Alexander](https://en.wikipedia.org/wiki/Christopher_Alexander) 162 | 163 | ## RAII 164 | 165 | 资源获取是初始化(RAII),代表C++中的一种流行技术,在这种技术中,资源的获取和释放与对象的生命周期绑定在一起。这意味着对于锁,互斥锁将被锁定在构造函数中,并在析构函数中解锁。这种RAII实现,也称为范围锁定。 166 | 167 | C++中的典型用例有:管理互斥锁生命周期的锁、管理资源(内存)生命周期的智能指针,或者管理元素生命周期的[标准模板库容器](https://en.cppreference.com/w/cpp/container)。 168 | 169 | ## 释放序列 170 | 171 | 原子对象M的释放序列,以释放操作A为首,是M修改顺序中最大的连续子序列,其中第一个操作为A,每个后续操作为: 172 | 173 | * 由执行A操作的线程进行的操作 174 | * 原子的读-改-写操作。 175 | 176 | ## 顺序一致的存储模型 177 | 178 | 顺序一致有两个基本特征: 179 | 180 | 1. 程序的指令是按源代码顺序执行的。 181 | 2. 所有线程上的所有操作都遵循全局顺序。 182 | 183 | ## 序列点 184 | 185 | 序列点定义了程序执行过程中的任何一个结点。在这个点上,可以保证先前评估的所有执行效果,而不影响后续评估的 执行效果。 186 | 187 | ## 伪唤醒 188 | 189 | 伪唤醒是一种条件变量的现象。可能发生的情况是,条件变量的等待组件错误地获取了一个通知。 190 | 191 | ## 线程 192 | 193 | 计算机科学中,执行线程是可由调度器独立管理的最小程序指令序列,调度器通常是操作系统的一部分。线程和进程的实现在不同的操作系统之间是不同的,但是在大多数情况下,线程是进程的一个组件。多个线程可以存放在于一个进程中,并发执行并共享内存等资源,而不同的进程不共享这些资源。特别是,进程中的线程在任何给定时间,共享其可执行代码和变量。想要了解更多信息,可以阅读维基百科关于[线程](https://en.wikipedia.org/wiki/Thread_(computing))的文章。 194 | 195 | ## 全序关系 196 | 197 | 总序是一个二元关系(<=)在某个集合X上表现,其有反对称性、传递性,完全性。 198 | 199 | * 反对称性:如果a <= b并且b <= a,则a == b 200 | * 传递性:如果a <= b, b <= c,则a <= c 201 | * 完全性:a <= b或b <= a 202 | 203 | ## volatile 204 | 205 | volatile通常用于表示可以独立于常规程序流进行更改的对象。例如,这些对象在嵌入式编程中表示一个外部设备(内存映射I/O)。由于这些对象可以独立于常规程序流进行更改,并且其值可以直接写入主内存,因此不会在缓存中进行优化存储。 206 | 207 | ## 无等待 208 | 209 | 当有每个线程都有进程保证不会互相影响时,那么一个非阻塞算法是无等待的。 210 | 211 | -------------------------------------------------------------------------------- /content/History-Quick-Overview.md: -------------------------------------------------------------------------------- 1 | # C++并行历史概述 2 | 3 | 随着C++11的发布,C++标准添加了多线程和内存模型。这样,标准库有了基本的构建块,比如:原子变量、线程、锁和条件变量。C++11提供了比引用更抽象的构建块,这是未来C++标准(C++20/23)能建立更高抽象的基础。 4 | 5 | ![](../images/History-Quick-Overview/0.png) 6 | 7 | 粗略地说,可以将C++并发分为三个演化过程。 8 | 9 | ## C++11和C++14: 铺垫 10 | 11 | C++11引入多线程,包括两个部分:良好的内存模型和标准化的线程接口。C++14为C++的多线程功能增加了读写锁。 12 | 13 | ### 内存模型 14 | 15 | 多线程的基础,是定义良好的内存模型。内存模型需要处理以下几个方面的内容: 16 | 17 | * 原子操作: 不受中断地操作。 18 | * 部分排序运算: 不能重排序的操作序列。 19 | * 操作的可见效果: 保证其他线程可以看到对共享变量的操作。 20 | 21 | C++内存模型的灵感来自于Java。然而,与Java的内存模型不同,C++允许打破顺序一致性的约束(原子操作的默认方式)。 22 | 23 | 顺序一致性提供了两个保证: 24 | 25 | 1. 程序指令按源码顺序执行。 26 | 2. 线程上的所有操作都遵循一个全局顺序。 27 | 28 | 内存模型基于原子数据类型(短原子)的原子操作。 29 | 30 | ### 原子类型 31 | 32 | C++有一组基本的原子数据类型,分别是布尔值、字符、数字和指针的变体。可以使用类模板`std::atomic`来定义原子数据类型。原子类型可以建立同步和排序约束,也适用于非原子类型。 33 | 34 | 标准化线程接口是C++并发的核心。 35 | 36 | ### 多线程 37 | 38 | C++中的多线程由线程、(共享数据的)同步语义、线程本地数据和任务组成。 39 | 40 | ### 线程 41 | 42 | `std::thread`表示一个独立的程序执行单元。执行单元,表示可接受调用的单元。可调用单元可以是函数名、函数对象或Lambda函数。 43 | 44 | 新线程的可执行单元结束时,要么进行等待主线程完成(`t.join()`),要么从主线程中分离出来(`t.detach()`)。如果没有对线程`t`执行`t.join()`或`t.detach()`操作,则线程`t`是可汇入的(joinable)。如果可汇入线程进行销毁时,会在其析构函数中调用`std::terminate`,则程序终止。 45 | 46 | 分离的线程在后台运行,通常称为**守护线程**。 47 | 48 | `std::thread`是一个可变参数模板,它可以接收任意数量的参数。 49 | 50 | #### 共享数据 51 | 52 | 如果多个线程同时使用共享变量,并且该变量是可变的(非const),则需要协调对该变量的访问。同时读写共享变量是一种数据竞争,也是一种未定义的行为。在C++中,可以通过锁(或互斥锁)来协调对共享变量的访问。 53 | 54 | #### 互斥锁 55 | 56 | 互斥锁(互斥量)保证在任何给定时间内,只有一个线程可以访问共享变量。互斥锁锁定/解锁共享变量所属的临界区(C++有5个不同的互斥对象)。即使互斥锁同时共享一个锁,也可以递归地、试探性地、有或没有时间限制地进行锁定。 57 | 58 | #### 锁 59 | 60 | 应该将互斥锁封装在锁中,从而自动释放互斥锁。锁通过将互斥锁的生命周期绑定到自己的生命周期来实现RAII。C++中`std::lock_guard`/`std::scoped_lock`可用于简单场景,`std::unique_lock`/`std::shared_lock`用于高级场景,例如:显式锁定或解锁互斥锁。 61 | 62 | ### 线程本地数据 63 | 64 | 将变量声明为`thread-local`可以确保每个线程都有变量的副本。线程本地数据的生存周期,与线程的生存周期相同。 65 | 66 | ### 条件变量 67 | 68 | 条件变量允许通过消息机制对线程进行同步。一个线程为发送方,而另一个线程为接收方,其中接收方阻塞等待来自发送方的消息。条件变量的典型用例是"生产者-消费者"模式。条件变量可以是发送方,也可以是接收方。正确使用条件变量非常具有挑战性。所以,这样的任务通常有更简单的解决方案。 69 | 70 | ### 任务 71 | 72 | 任务与线程有很多共同之处。虽然显式地创建了一个线程,但任务只是工作的开始。C++运行时会自动处理任务的生存期,比如:`std::async`。 73 | 74 | 任务就像两个通信端点之间的数据通道。支持线程之间的安全通信,当一个端点将数据放入数据通道时,另一个端点将在未来某个时刻获取该值。数据可以是值、异常或通知。除了`std::async`, C++还有`std::promise`和`std::future`,这两个类模板可以对任务有更多的控制。 75 | 76 | ## C++17: 标准模板库算法的并行 77 | ![](../images/History-Quick-Overview/1.png) 78 | 79 | C++17的并发性发生了巨大的变化,特别是标准模板库(STL)的并行算法。C++11和C++14只提供了并发性的基础构建块。这些工具适合库或框架开发人员,但不适合应用程序开发人员。C++11和C++14中的多线程,在C++ 17中的并发性面前,相当于汇编语言! 80 | 81 | ### 执行策略 82 | 83 | C++17中,大多数STL算法都有并行实现,这样就可以使用执行策略来调用算法。该策略指定算法是串行执行(`std::execution::seq`)、并行执行(`std::execution::par`),还是与向量化的并行执行(`std::execution::par_unseq`)。 84 | 85 | ### 新算法 86 | 87 | 除了在重载,并行了原始的69种算法,还添加了8种新算法。这些新算法非常适合并行归约、扫描或转换。 88 | 89 | ## 案例研究 90 | 91 | 介绍了内存模型和多线程接口的理论知识之后,会将这些知识应用到一些案例中。 92 | 93 | ### 求向量元素的加和 94 | 95 | 计算一个向量的加和有多种方法。可以串行执行,也可以通过数据共享并发执行,不同的实现方式,性能上有很大的差别。 96 | 97 | ### 单例:线程安全的初始化 98 | 99 | 单例对象的初始化是线程安全的,是共享变量线程安全初始化的经典案例。有许多实现方法可以做到这一点,不过在性能上有一定的差异。 100 | 101 | ### 使用CppMem进行优化 102 | 103 | 我会从一个小程序开始,然后不断地改进它,并用CppMem验证优化过程的每个步骤。 [CppMem](http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem)是一个交互式工具,用于研究小代码段的C++内存模型行为。 104 | 105 | ## C++20/23: 并发的未来 106 | 107 | ![](../images/History-Quick-Overview/2.png) 108 | 109 | 对未来的标准预测非常难([Niels Bohr](https://en.wikipedia.org/wiki/Niels_Bohr)),这里描述了C++20/23的并发特性。 110 | 111 | ### Executors 112 | 113 | Executor由一组如何运行可调用单元的规则组成。它们指定执行是否应该在线程、线程池,甚至单线程(无并发)上运行(可调用的)基础构建块上进行。提案[N4734](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4734.pdf)的扩展依赖于扩展的future,也依赖于STL的并行算法,以及C++20/23中新的并发特性,如:门闩和栅栏、协程、事务性内存和任务块(最终都会使用它们)。 114 | 115 | #### std::jthread 116 | 117 | `std::jthread`是`std::thread`的增强版。除了`std::thread`外,`std::jthread`还可以发出中断信号,并自动并入启动的线程。 118 | 119 | #### 原子智能指针 120 | 121 | 智能指针[`std::shared_ptr`](http://en.cppreference.com/w/cpp/memory/shared_ptr)和[`std::weak_ptr`](http://en.cppreference.com/w/cpp/memory/weak_ptr)在并发程序中存在概念问题。它们的本质上是共享的,这就使得状态可变,所以容易出现数据竞争,从而导致未定义的行为。`std::shared_ptr`和`std::weak_ptr`保证引用计数器的递增或递减是一个原子操作。资源只被删除一次,但不能保证对资源访问的原子性。新的原子智能指针`std::atomic>`和`std::atomic>`解决了这个问题。两者都是`std::atomic`的偏特化版本。 122 | 123 | #### 扩展版future 124 | 125 | C++11引入了promise和future,其有很多优点,但也有一个缺点:不能组合成强大的工作流。在C++20/23中,future应该会消弭这个缺点。 126 | 127 | #### 门闩和栅栏 128 | 129 | C++14没有信号量,而信号量是用于限制访问资源的利器。因为C++20/23提出了门闩和屏障,就不用担心没有信号量可用的问题了。可以使用门闩和栅栏在异步点进行等待,直到计数器变为零。门闩和栅栏的区别在于,`std::latch`只能使用一次,而`std::barrier`和`std::flex_barrier`可以使用多次。与`std::barrier`不同,`std::flex_barrier`可以在每次迭代之后调整它的计数器。 130 | 131 | #### 协程 132 | 133 | 协程是可以挂起,并保持执行函数的状态。协程通常在操作系统、事件循环、无限列表或管道中使用,用于实现需要协作才能完成的任务。 134 | 135 | #### 事务内存 136 | 137 | 事务内存基于数据库理论中事务的基本思想。事务是一种操作,它提供了ACID数据库事务的前三个属性:原子性、一致性和隔离性。数据库特有的持久性不适用C++的事务内存。新标准有两种类型的事务内存:同步块和原子块。它们都按总顺序执行的,表现得好像有一个全局锁在保护它们。与同步块相比,原子块不能执行事务不安全的代码。 138 | 139 | #### 任务块 140 | 141 | 任务块在C++中实现了fork-join范式。下图说明了任务块的关键思想:启动任务的fork阶段和同步任务的join阶段。 142 | 143 | ![](../images/History-Quick-Overview/3.png) 144 | 145 | ### 模式和最佳实践 146 | 147 | 模式是从实践中记录下来的最佳方式。[Christopher Alexander](https://en.wikipedia.org/wiki/Christopher_Alexander)说,“模式表达了特定环境、问题和解决方案之间的关系“。从更概念化的角度看待并发编程,会得到更多解决问题的方式。与更概念化的并发模式相比,本章提供了面对并发挑战的实用技巧。 148 | 149 | #### 同步 150 | 151 | 数据竞争的必要前提是数据处于共享的、可变状态。同步模式可以归结为两个问题:处理共享和处理可变。 152 | 153 | #### 并行架构 154 | 155 | 并发架构章节中介绍了三种模式。前两种模式是活动对象和监视器对象的同步,以及调度器方法的使用。第三种半同步/半异步模式关注体系结构,并在并发系统中解耦异步和同步(服务)的处理。 156 | 157 | #### 最佳实践 158 | 159 | 并发编程比较复杂,因此通过最佳实践,可以更多的了解多线程和内存模型。 160 | 161 | ###数据结构 162 | 163 | #### 挑战项目 164 | 165 | 编写并发程序本来就很复杂,使用C++11和C++14的特性也是如此。因此,我将详细描述具挑战性的问题。希望用一整章的篇幅来讨论并发编程的挑战,会让你更清楚其中的陷阱。这里有竞争条件、数据竞争和死锁等挑战项目。 166 | 167 | #### 计时库 168 | 169 | 计时库是C++并发工具的重要组成部分。通常,可以让线程在特定的时间内处于休眠状态,或者一直休眠到特定的时间点。计时库包括:时间点、时间段和时钟。 170 | 171 | #### CppMem 172 | 173 | CppMem是一个交互式工具,用于深入了解内存模型。它提供了两项非常有价值的服务:可以验证无锁代码,可以分析无锁代码,并且能得到对代码的鲁棒性有更多的理解。本书会经常使用CppMem。由于CppMem的配置选项和见解非常具有挑战性,也会提供相应章节,以便对CppMem有一些基本的了解。 174 | 175 | #### 术语表 176 | 177 | 术语表对最基本的术语作了简单的解释。 -------------------------------------------------------------------------------- /content/How-you-should-read-the-book.md: -------------------------------------------------------------------------------- 1 | # 如何阅读 2 | 3 | 如果对C++的并发性不是很熟悉,可以从最开始的部分开始,先快速地了解一下。 4 | 5 | 当有了大概的了解,就可以着手处理细节。第一遍阅读可以先跳过内存模型,不过在案例研究章节将之前的理论进行实践,因为需要对内存模型所有理解,所以非常有挑战性。 6 | 7 | "未来:C++20/23"是可选择性阅读章节。我对未来非常好奇,希望你和我一样! 8 | 9 | 最后,为了更好地理解书中的内容,并充分利用这些知识,本书还提供了额外的应用指导。 -------------------------------------------------------------------------------- /content/Pattrns/Best-Practices/9.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 最佳实践 2 | 3 | 本章提供了一组简单的规则,可用于在现代C++中编写良好且快速的并发程序。多线程的并行性和并发性,在C++中算是个比较新的主题,在未来将发现越来越多的最佳实践方式。规则会随着时间推移而发展,所以不要把本章的规则看作一个完整的列表,而是作为一个起点,对于并行STL尤其如此。在更新这本书的时候(2018年12月),C++17的并行算法只是部分可用,所以现在为它定制最佳实践还为时过早。 -------------------------------------------------------------------------------- /content/Pattrns/Best-Practices/9.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 通常情况 2 | 3 | 我们先从一些原子操作和线程操作的最佳实践开始。 4 | 5 | ## 代码评审 6 | 7 | 代码评审应该是专业软件开发过程必备的一部分,尤其是处理并发。并发性本质上非常复杂,需要深思熟虑的分析和经验。 8 | 9 | 为了使评审更有效,请在评审之前将想要讨论的代码发送给评审人员,并声明代码中哪些地方是不可变的。正式评审开始之前,应该给予评审员足够的时间来分析代码。 10 | 11 | 不知道怎么做?举个例子。还记得`std::shared_lock`一章readerWriterLock.cpp中的数据竞争吗? 12 | 13 | ```c++ 14 | // readerWriterLock.cpp 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | std::map teleBook{ {"Dijkstra", 1972}, {"Scott", 1976}, 23 | {"Ritchie", 1983} }; 24 | 25 | std::shared_timed_mutex teleBookMutex; 26 | 27 | void addToTeleBook(const std::string& na, int tele) { 28 | std::lock_guard writerLock(teleBookMutex); 29 | std::cout << "\nSTARTING UPDATE " << na; 30 | std::this_thread::sleep_for(std::chrono::milliseconds(500)); 31 | teleBook[na] = tele; 32 | std::cout << " ... ENDING UPDATE " << na << std::endl; 33 | } 34 | 35 | void printNumber(const std::string& na) { 36 | std::shared_lock readerLock(teleBookMutex); 37 | std::cout << na << ": " << teleBook[na]; 38 | } 39 | 40 | int main() { 41 | 42 | std::cout << std::endl; 43 | 44 | std::thread reader1([] {printNumber("Scott"); }); 45 | std::thread reader2([] {printNumber("Ritchie"); }); 46 | std::thread w1([] {addToTeleBook("Scott",1968); }); 47 | std::thread reader3([] {printNumber("Dijkstra"); }); 48 | std::thread reader4([] {printNumber("Scott"); }); 49 | std::thread w2([] {addToTeleBook("Bjarne", 1965); }); 50 | std::thread reader5([] {printNumber("Scott"); }); 51 | std::thread reader6([] {printNumber("Ritchie"); }); 52 | std::thread reader7([] {printNumber("Scott"); }); 53 | std::thread reader8([] {printNumber("Bjarne"); }); 54 | 55 | reader1.join(); 56 | reader2.join(); 57 | reader3.join(); 58 | reader4.join(); 59 | reader5.join(); 60 | reader6.join(); 61 | reader7.join(); 62 | reader8.join(); 63 | w1.join(); 64 | w2.join(); 65 | 66 | std::cout << std::endl; 67 | 68 | std::cout << "\nThe new telephone book" << std::endl; 69 | for (auto teleIt : teleBook) { 70 | std::cout << teleIt.first << ": " << teleIt.second << std::endl; 71 | } 72 | 73 | std::cout << std::endl; 74 | 75 | } 76 | ``` 77 | 78 | 问题在于第24行`teleBook[na]`,这是一个可以修改的电话簿。可以通过将读取线程`reader8`放在其他读取线程之前,来触发数据竞争。在我的C++研讨会上,这个程序作为发现数据竞争的一种练习,大约10%的参与者在5分钟内能发现数据竞争。 79 | 80 | ## 尽量减少可变数据的共享 81 | 82 | 应该尽量减少可变数据的共享,原因有两个:性能和安全性。安全性主要是关于数据竞争,这里我们来详谈一下性能。 83 | 84 | 在计算向量和的章节中,我们做了详尽的性能研究。展示了将`std::vector`的值加起来要花费多少时间。 85 | 86 | 下面是单线程求和的关键部分。 87 | 88 | ```c++ 89 | ... 90 | 91 | constexpr long long size = 100000000; 92 | 93 | std::cout << std::endl; 94 | 95 | std::vector randValues; 96 | randValues.reserve(size); 97 | 98 | // random values 99 | std::random_device seed;std::mt19937 engine(seed()); 100 | std::uniform_int_distribution<> uniformDist(1, 10); 101 | 102 | const unsigned long long sum = std::accumulate(randValues.begin(), randValues.end(), 0); 103 | 104 | ... 105 | ``` 106 | 107 | 然后,在四个线程上执行求和,并很天真地使用了一个共享的求和变量。 108 | 109 | ```c++ 110 | ... 111 | void sumUp(unsigned long long& sum, const std::vector& val, 112 | unsigned long long beg, unsigned long long end){ 113 | for (auto it = beg; it < end; ++it){ 114 | std::lock_guard myLock(myMutex); 115 | sum += val[it]; 116 | } 117 | } 118 | ... 119 | ``` 120 | 121 | 后来,通过使用原子变量求和。 122 | 123 | ```c++ 124 | ... 125 | void sumUp(std::atomic& sum, const std::vector& val, 126 | unsigned long long beg, unsigned long long end){ 127 | for (auto it = beg; it < end; ++it){ 128 | sum.fetch_add(val[it]); 129 | } 130 | } 131 | ... 132 | ``` 133 | 134 | 最后,通过计算局部和,得到了性能的提升。 135 | 136 | ```c++ 137 | ... 138 | void sumUp(unsigned long long& sum, const std::vector& val, 139 | unsigned long long beg, unsigned long long end){ 140 | unsigned long long tmpSum{}; 141 | for (auto i = beg; i < end; ++i){ 142 | tmpSum += val[i]; 143 | } 144 | std::lock_guard lockGuard(myMutex); 145 | sum += tmpSum; 146 | } 147 | ... 148 | ``` 149 | 150 | 性能数字令人印象深刻,并提供了明确的指示。求和变量共享的部分越少,从多线程中获得性能收益越高。 151 | 152 | | 单线程 | std::lock_guard | 原子变量 | 本地求和 | 153 | | :------: | :-------------: | :------: | :------: | 154 | | 0.07 sec | 3.34 sec | 1.34 sec | 0.03 sec | 155 | 156 | ## 减少等待 157 | 158 | 你可能听说过[阿姆达尔定律](https://en.wikipedia.org/wiki/Amdahl%27s_law)。它预测了使用多个处理器可以获得的理论上的最大加速比。定律很简单,如果p是可以并发运行的代码的比例,则可以获得最大的加速$\frac{1}{1-p}$。因此,如果90%的代码可以并发运行,就可以 159 | 得到(最多)10倍的加速$\frac{1}{1-p}==\frac{1}{1-0.9}==\frac{1}{0.1}==10$。 160 | 161 | 反过来看,如果使用锁导致10%的代码必须串行,那么最多可以获得10倍的加速。当然,这里假设可以访问的处理资源是无限制的。 162 | 163 | 该图清楚地显示了Amdahl定律的曲线。 164 | 165 | ![](../../../images/Patterns/Best-Practices/1.png) 166 | 167 | By Daniels220 at English Wikipedia, CC BY-SA 3.0, https://commons.wikimedia.org/w/index.php?curid=6678551 168 | 169 | 核心的最佳数量在很大程度上取决于代码的并行部分。例如:如果有50%的并行代码,那么就可以用16个核芯可达到最高的性能,使用过多的内核会使程序运行速度变慢。如果您有95%的并行代码,那么使用2048个核芯可将性能达到峰值。 170 | 171 | ## 不可变数据 172 | 173 | 数据竞争是指,至少两个线程同时访问一个共享变量的情况,并且至少有一个线程尝试修改该变量。数据竞争的一个必要条件是可变的共享状态,下面的图表清楚地说明了我的观点。 174 | 175 | ![](../../../images/Patterns/Best-Practices/2.png) 176 | 177 | 如果没有不可变的数据,则不会发生数据竞争。只需确保不可变数据以线程安全的方式初始化即可。在线程安全初始化的章节中,介绍了四种方法来保证这一点,这里复述一下: 178 | 179 | * 线程创建前进行初始化。 180 | * 常数表达式。 181 | * `std::call_once`与`std::once_flag`的组合。 182 | * 具有块作用域的静态变量。 183 | 184 | C++中创建不可变数据的两种方法:`const`和`constexpr`。`const`是一种运行时技术,而`constexpr`可保证该值在编译时初始化,因此是线程安全的。甚至自定义的类型,也可以在编译时初始化。 185 | 186 | **自定义的类型** 187 | 188 | 对于用户定义的类型,在编译时创建实例,会有一些限制。 189 | 190 | `constexpr`的构造函数的限制: 191 | 192 | * 只能用常量表达式。 193 | * 不能使用异常处理。 194 | * 必须声明为默认或删除,否则函数体必须为空(C++11)。 195 | 196 | 自定义的`constexpr`类型的限制: 197 | 198 | * 不能有虚拟基类。 199 | * 要求每个基对象和每个非静态成员必须在构造函数的初始化列表中初始化,或者直接在类体中初始化。因此,使用的构造函数(例如基类的构造函数)必须是`constexpr`,而且必须使用常量表达式进行初始化。 200 | 201 | [cppreference.com](https://en.cppreference.com/w/cpp/language/constexpr)为`constexpr`自定义类型提供了更多的信息。为了将实践添加到理论中,我定义了`MyInt`类,`MyInt`涉及到了刚刚提到的点,还有`constexpr`方法。 202 | 203 | ```c++ 204 | // userdefinedTypes.cpp 205 | 206 | #include 207 | #include 208 | 209 | class MyInt { 210 | public: 211 | constexpr MyInt() = default; 212 | constexpr MyInt(int fir, int sec) :myVal1(fir), myVal2(sec) {} 213 | MyInt(int i) { 214 | myVal1 = i - 2; 215 | myVal2 = i + 3; 216 | } 217 | 218 | constexpr int getSum() const { return myVal1 + myVal2; } 219 | 220 | friend std::ostream& operator<<(std::ostream& out, const MyInt& myInt) { 221 | out << "(" << myInt.myVal1 << "," << myInt.myVal2 << ")"; 222 | return out; 223 | } 224 | 225 | private: 226 | int myVal1 = 1998; 227 | int myVal2 = 2003; 228 | 229 | }; 230 | 231 | int main() { 232 | 233 | std::cout << std::endl; 234 | 235 | constexpr MyInt myIntConst1; 236 | 237 | constexpr int sec = 2014; 238 | constexpr MyInt myIntConst2(2011, sec); 239 | std::cout << "myIntConst2.getSum(): " << myIntConst2.getSum() << std::endl; 240 | 241 | int arr[myIntConst2.getSum()]; 242 | static_assert(myIntConst2.getSum() == 4025, "2011 + 2014 should be 4025"); 243 | 244 | std::cout << std::endl; 245 | 246 | } 247 | ``` 248 | 249 | `MyInt`类有两个`constexpr`构造函数。一个默认构造函数(第8行)和一个接受两个参数的构造函数(第9行)。另外,该类有两个方法,但是只有`getSum`方法是常量表达式。因为`constexpr`方法在C++11和C++14是不同的,不会自动进行`const`修饰,所以方法声明为`const`。如果在`constexpr`对象中使用变量`myVal1`和`myVal2`(第23行和第24行),有两种方法可以定义它们。首先,可以在构造函数的初始化列表中初始化它们(第9行);其次,可以在类体中初始化它们(第23行和第24行)。这里,构造函数的初始化列表中的初始化具有更高的优先级。 250 | 251 | 第38行和第39行中可以在一个常量表达式中调用`constexpr`方法。下面是程序的输出。 252 | 253 | ![](../../../images/Patterns/Best-Practices/3.png) 254 | 255 | 再次强调:`constexpr`对象只能使用`constexpr`方法初始化。 256 | 257 | 像Haskell这样没有可变数据的函数式编程语言,则非常适合并发编程。 258 | 259 | ## 使用纯函数 260 | 261 | Haskell被称为纯函数语言,纯函数是在给定相同参数时,总是产生相同结果的函数。它没有副作用,因此不能改变程序的状态。 262 | 263 | 从并发性的角度来看,纯函数具有明显的优势。它们可以重新排序,也可以在另一个线程上自动运行。 264 | 265 | C++中的函数默认不是纯函数。以下三个函数都是纯函数,但每个函数都有不同的特征。 266 | 267 | ```c++ 268 | int powFunc(int m, int n){ 269 | if (n == 0) return 1; 270 | return m * powFunc(m, n-1); 271 | } 272 | ``` 273 | 274 | `powFunc`是一个普通函数。 275 | 276 | ```c++ 277 | template 278 | struct PowMeta{ 279 | static int const value = m * PowMeta::value; 280 | }; 281 | 282 | template 283 | struct PowMeta{ 284 | static int const value = 1; 285 | }; 286 | ``` 287 | 288 | `PowMeta`是一个元函数(meta-function),因为它在编译时运行。 289 | 290 | ```c++ 291 | constexpr int powConst(int m, int n){ 292 | int r = 1; 293 | for(int k = 1; k <= n; ++k) r *= m; 294 | return r; 295 | } 296 | ``` 297 | 298 | `powCont`函数可以在运行时和编译时运行,它是一个常量函数。 299 | 300 | ## 寻找正确的抽象概念 301 | 302 | 多线程环境中,有多种方法可以初始化单例。可以使用标准库中的`lock_guard`或`std::call_once`,或使用依赖于核心语言的静态变量,亦或是使用依赖于原子变量的获取-释放语义。显然,使用获取-释放语义最具挑战性。使用者必须执行它,维护它,还要向同事解释它。与这些工作相比,Meyers单例在更容易实现,并且运行速度更快。 303 | 304 | 可以使用`std::reduce`,而不是实现一个并行循环进行求和。可以使用二元操作可调用和并行执行策略,对`std::reduce`进行参数化。 305 | 306 | 越是追求正确的抽象,工作就会越轻松。 307 | 308 | ## 使用静态代码分析工具 309 | 310 | 案例分析章节中,我介绍了CppMem。[CppMem](http://svr-pes20-cppmem.cl.cam.ac.uk/cppmem/)是一个交互式工具,用于对小代码段的C++内存模型,进行行为研究。CppMem可以提供两个方面的帮助:首先,可以验证代码的正确性;其次,可以更深入地了解内存模型,从而更全面地了解多线程问题。 311 | 312 | ## 使用动态执行工具 313 | 314 | [ThreadSanitizer](https://github.com/google/sanitizers/wiki/ThreadSanitizerCppManual)是一个针对C/C++的数据竞争探测器。ThreadSanitizer已经作为Clang 3.2和GCC 4.8的一部分。要使用ThreadSanitizer,必须使用编译标志`-fsanitize=thread`来编译和链接你的程序。 315 | 316 | 下面的程序有一个数据竞争。 317 | 318 | ```c++ 319 | // dataRace.cpp 320 | 321 | #include 322 | 323 | int main() { 324 | 325 | int globalVar{}; 326 | 327 | std::thread t1([&globalVar] { ++globalVar; }); 328 | std::thread t2([&globalVar] { ++globalVar; }); 329 | 330 | t1.join(); 331 | t2.join(); 332 | 333 | } 334 | ``` 335 | 336 | `t1`和`t2`同时访问`globalVar`,两个线程都试图修改`globalVar`。让我们编译并运行该程序。 337 | 338 | `g++ -std=c++11 dataRace.cpp -fsanitize=thread -pthread -g -o dataRace` 339 | 340 | 这个程序的输出相当冗长。 341 | 342 | ![](../../../images/Patterns/Best-Practices/4.png) 343 | 344 | 我用红色框突出了屏幕截图的关键段,这段表示在源码第10行有一个数据竞争。 -------------------------------------------------------------------------------- /content/Pattrns/Best-Practices/9.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 内存模型 2 | 3 | 多线程的基础是定义良好的内存模型。对内存有基本的了解,有助于更深入地了解多线程的挑战。 4 | 5 | ## 不要使用volatile进行同步 6 | 7 | C++与C#或Java相比,`volatile`关键字没有多线程语义。在C#或Java中,`volatile`声明了一个原子变量,如`std::atomic`在C++中声明了一个原子一样,通常用于可以进行更改的对象。由于这一特性,没有优化的存储会发生在缓存中。 8 | 9 | ## 不要让程序无锁 10 | 11 | 这个建议听起来很荒谬,但是这个建议的理由很简单,无锁编程非常容易出错,并且需要在这个领域是专家级别的人,才能保证很少出错。如果需要实现无锁的数据结构,请务必注意ABA问题。 12 | 13 | ## 如果使用无锁程序,请使用成熟的模式 14 | 15 | 如果已经确定要使用无锁方案,那么请使用成熟的模式。 16 | 17 | 1. 简单的共享原子布尔值或原子计数器。 18 | 2. 使用线程安全,甚至无锁的容器来支持消费者/生产者的场景。如果使用的容器是线程安全的,则可以将值放入容器中或从容器中取出,而不必担心同步的问题。这就将应用程序的挑战转移到基础设施中。 19 | 20 | ## 不要构建自定义的抽象方式,尽量使用当前语言能够保证的方式 21 | 22 | 共享变量的线程安全初始化,可以通过多种方式完成。可以依赖于C++运行时的保证,比如:常量表达式、带有块作用域的静态变量,或者使用函数`std::call_once`与`std::once_flag`组合使用。这里用C++编程,即使使用非常复杂的获取-发布语义,也可以构建基于原子的抽象。一开始最好不要这样做,除非不得已。这意味着,通过度量关键代码的性能来确定瓶颈时,只有当明确自定义版本比当前语言默认的方式性能更好时,再进行更改。 23 | 24 | ## 不要重新发明轮子 25 | 26 | 编写线程安全的数据结构是一项颇具挑战性的工作,这要比编写无锁的数据结构更困难。因此,最好使用现成的库,如[Boost.Lockfree](http://www.boost.org/doc/libs/1_66_0/doc/html/lockfree.html)或[CDS](http://http://libcds.sourceforge.net/). 27 | 28 | **Boost.Lockfree** 29 | 30 | Boost.Lockfree支持三种不同的数据结构: 31 | 32 | Queue:无锁的多生产/多消费者队列 33 | 34 | Stack:无锁的多产品/多消费者堆栈 35 | 36 | spsc_queue:无等待的单生产者/单消费者队列(通常称为环形缓冲区) 37 | 38 | **CDS** 39 | 40 | CDS代表并发数据结构,包含许多侵入式(非拥有)和非侵入式(拥有)容器。因为它们会自动管理元素,所以标准模板库的容器是非侵入的。 41 | 42 | * 堆栈(无锁) 43 | * 队列和带优先级的队列 (无锁) 44 | * 有序列表 45 | * 有序的set和map(无锁和有锁) 46 | * 无序的set和map(无锁和有锁 ) -------------------------------------------------------------------------------- /content/Pattrns/Concurrent-Architecture/8.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 并发架构 2 | 3 | 本章介绍三种经典架构模式,在[《面向模式的软件体系结构:并发和网络对象的模式》]( https://www.dre.vanderbilt.edu/~schmidt/POSA/POSA2/)中都有很好的解释。本章会简单概述一下活动对象、监控对象和半同步/半异步模式。在同步模式中,我会使用C++作为第一视角。在深入研究这三种模式之前,先做对这几个模式进行简单的介绍。 4 | 5 | * 活动对象的设计模式将执行与调用进行解耦,每个对象会留在自己的控制线程中,其目标是通过使用异步方法和调度器来引入并发。维基百科:[Active object]( https://en.wikipedia.org/wiki/Active_object) 6 | * 监控对象的设计模式,会同步并发方法的执行,以确保对象每次只运行一个成员函数。并且,还允许对象的成员函数协同调度序列的执行。 7 | 8 | 这两种模式可以以同步和调度的方式运行。主要的区别是,活动对象在不同的线程中执行,而监控对象与客户端则是在相同的线程中执行。与关注子系统的活动对象和监控对象(因此通常称为设计模式)不同,以下的体系结构模式具有系统视角。 9 | 10 | * 半同步/半异步体系结构模式,在并发系统中对异步和同步服务处理进行解耦,从而在不降低太多性能的情况下简化编程。该模式引入了两个通信层,一个用于异步,另一个用于同步。 11 | 12 | -------------------------------------------------------------------------------- /content/Pattrns/Concurrent-Architecture/8.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 活动对象 2 | 3 | 活动对象模式将执行与对象的成员函数解耦,每个对象会留在在自己的控制线程中。其目标是通过使用异步方法,处理调度器的请求,从而触发并发。维基百科:[Active object]( https://en.wikipedia.org/wiki/Active_object)。所以,这种模式也称为并发对象模式。 4 | 5 | 客户端的调用会转到代理,代理表现为活动对象的接口。服务提供活动对象的实现,并在单独的线程中运行。代理在运行时将客户端的调用转换为对服务的调用,调度程序将方法加入到激活列表中。调度器与服务在相同的线程中活动,并将方法调用从激活列表中取出,再将它们分派到相应的服务上。最后,客户端可以通过future从代理处获取最终的结果。 6 | 7 | ## 组件 8 | 9 | 活动对象模式由六个组件组成: 10 | 11 | 1. 代理为活动对象的可访问方法提供接口。代理将触发激活列表的方法,并请求对象的构造。并且,代理和客户端运行在相同的线程中。 12 | 2. 方法请求类定义了执行活动对象的接口。 13 | 3. 激活列表的目标是维护挂起的请求,激活列表将客户端线程与活动对象线程解耦。代理对入队请求的进行处理,而调度器将请求移出队列。 14 | 4. 调度器与代理可在不同的线程中运行。调度器会在活动对象的线程中运行,并决定接下来执行激活列表中的哪个请求。 15 | 5. 可以通过服务实现活动对象,并在活动对象的线程中运行,服务也支持代理接口。 16 | 6. future是由代理创造的,客户端可以从future上获取活动对象调用的结果。客户端可以安静等待结果,也可以对结果进行轮询。 17 | 18 | 下面的图片显示了消息的顺序。 19 | 20 | ![](../../../images/Patterns/Concurrent-Architecture/1.png) 21 | 22 | > **代理** 23 | > 24 | > 代理设计模式是[《设计模式:可重用的面向对象软件的元素》](https://en.wikipedia.org/wiki/Design_Patterns)中的经典模式,代理是其他对象的代表。典型的代理可以是远程代理[CORBA](https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture)、安全代理、虚拟代理或智能指针,如`std::shared_ptr`。每个代理会为它所代表的对象添加额外的功能。远程代理代表远程对象,并使客户端产生本地对象的错觉。安全代理通过对数据进行加密和解密,将不安全的连接转换为安全的连接。虚拟代理以惰性的方式封装对象的创建,智能指针将接管底层内存的生存期。 25 | > 26 | > ![](../../../images/Patterns/Concurrent-Architecture/2.png) 27 | > 28 | > * 代理具有与RealSubject相同的接口,用于管理引用,还有subject的生命周期。 29 | > * 与Subject具有相同的接口,如代理和RealSubject。 30 | > * RealSubject用于提供具体的功能。 31 | > 32 | > 关于代理模式的更多细节,可以参考[Wikipedia](https://en.wikipedia.org/wiki/Proxy_pattern)页面。 33 | 34 | ## 优点和缺点 35 | 36 | 介绍Active Object模式的最小实现前,先了解一下它的优点和缺点。 37 | 38 | * 优点: 39 | * 同步只需要在活动对象的线程上进行,不需要在客户端的线程上进行。 40 | * 客户端(用户)和服务器(实现者)之间的解耦,同步的挑战则在实现者的一边。 41 | * 由于客户端为异步请求,所以系统的吞吐量提高了,从而调用处理密集型方法不会阻塞整个系统。 42 | * 调度器可以实现各种策略来执行挂起请求,因此可以按不同的顺序执行入队请求。 43 | * 缺点: 44 | * 如果请求的粒度太细,则活动对象模式(如代理、激活列表和调度器)的性能开销可能过大。 45 | * 由于调度器的调度策略和操作系统的调度互相影响,调试活动对象模式通常非常困难,尤其是以不同顺序执行请求的情况下。 46 | 47 | ## 具体实现 48 | 49 | 下面的示例展示了活动对象模式的简单实现。我没有定义一个请求,这应该由代理和服务实现。而且,当请求调度程序执行下一个请求时,服务应该只执行这个请求。 50 | 51 | 所涉及的类型为`future>>>`,这个类型的标识有点长。为了提高可读性,我使用了声明(第16 - 37行)。 52 | 53 | ```c++ 54 | // activeObject.cpp 55 | 56 | #include 57 | #include 58 | #include 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | #include 65 | #include 66 | #include 67 | #include 68 | 69 | using std::async; 70 | using std::boolalpha; 71 | using std::cout; 72 | using std::deque; 73 | using std::distance; 74 | using std::endl; 75 | using std::for_each; 76 | using std::find_if; 77 | using std::future; 78 | using std::lock_guard; 79 | using std::make_move_iterator; 80 | using std::make_pair; 81 | using std::move; 82 | using std::mt19937; 83 | using std::mutex; 84 | using std::packaged_task; 85 | using std::pair; 86 | using std::random_device; 87 | using std::sort; 88 | using std::thread; 89 | using std::uniform_int_distribution; 90 | using std::vector; 91 | 92 | class IsPrime { 93 | public: 94 | pair operator()(int i) { 95 | for (int j = 2; j * j <= i; ++j) { 96 | if (i % j == 0)return std::make_pair(false, i); 97 | } 98 | return std::make_pair(true, i); 99 | } 100 | }; 101 | 102 | class ActivaeObject { 103 | public: 104 | 105 | future> enqueueTask(int i) { 106 | IsPrime isPrime; 107 | packaged_task(int)> newJob(isPrime); 108 | auto isPrimeFuture = newJob.get_future(); 109 | auto pair = make_pair(move(newJob), i); 110 | { 111 | lock_guard lockGuard(activationListMutex); 112 | activationList.push_back(move(pair)); 113 | } 114 | return isPrimeFuture; 115 | } 116 | 117 | void run() { 118 | thread servant([this] { 119 | while (!isEmpty()) { 120 | auto myTask = dequeueTask(); 121 | myTask.first(myTask.second); 122 | } 123 | }); 124 | servant.join(); 125 | } 126 | 127 | private: 128 | 129 | pair(int)>, int> dequeueTask() { 130 | lock_guard lockGuard(activationListMutex); 131 | auto myTask = std::move(activationList.front()); 132 | activationList.pop_front(); 133 | return myTask; 134 | } 135 | 136 | bool isEmpty() { 137 | lock_guard lockGuard(activationListMutex); 138 | auto empty = activationList.empty(); 139 | return empty; 140 | } 141 | 142 | deque(int)>, int >> activationList; 143 | mutex activationListMutex; 144 | }; 145 | 146 | vector getRandNumber(int number) { 147 | random_device seed; 148 | mt19937 engine(seed()); 149 | uniform_int_distribution<> dist(1000000, 1000000000); 150 | vector numbers; 151 | for (long long i = 0; i < number; ++i) numbers.push_back(dist(engine)); 152 | return numbers; 153 | } 154 | 155 | future>>> getFutures(ActivaeObject& activeObject, 156 | int numberPrimes) { 157 | return async([&activeObject, numberPrimes] { 158 | vector>> futures; 159 | auto randNumbers = getRandNumber(numberPrimes); 160 | for (auto numb : randNumbers) { 161 | futures.push_back(activeObject.enqueueTask(numb)); 162 | } 163 | return futures; 164 | }); 165 | } 166 | 167 | 168 | int main() { 169 | 170 | cout << boolalpha << endl; 171 | 172 | ActivaeObject activeObject; 173 | 174 | // a few clients enqueue work concurrently 175 | auto client1 = getFutures(activeObject, 1998); 176 | auto client2 = getFutures(activeObject, 2003); 177 | auto client3 = getFutures(activeObject, 2011); 178 | auto client4 = getFutures(activeObject, 2014); 179 | auto client5 = getFutures(activeObject, 2017); 180 | 181 | // give me the futures 182 | auto futures = client1.get(); 183 | auto futures2 = client2.get(); 184 | auto futures3 = client3.get(); 185 | auto futures4 = client4.get(); 186 | auto futures5 = client5.get(); 187 | 188 | // put all futures together 189 | futures.insert(futures.end(), make_move_iterator(futures2.begin()), 190 | make_move_iterator(futures2.end())); 191 | 192 | futures.insert(futures.end(), make_move_iterator(futures3.begin()), 193 | make_move_iterator(futures3.end())); 194 | 195 | futures.insert(futures.end(), make_move_iterator(futures4.begin()), 196 | make_move_iterator(futures4.end())); 197 | 198 | futures.insert(futures.end(), make_move_iterator(futures5.begin()), 199 | make_move_iterator(futures5.end())); 200 | 201 | // run the promises 202 | activeObject.run(); 203 | 204 | // get the results from the futures 205 | vector> futResults; 206 | futResults.reserve(futResults.size()); 207 | for (auto& fut : futures)futResults.push_back(fut.get()); 208 | 209 | sort(futResults.begin(), futResults.end()); 210 | 211 | // separate the primes from the non-primes 212 | auto prIt = find_if(futResults.begin(), futResults.end(), 213 | [](pairpa) {return pa.first == true; }); 214 | 215 | cout << "Number primes: " << distance(prIt, futResults.end()) << endl; 216 | cout << "Primes: " << endl; 217 | for_each(prIt, futResults.end(), [](auto p) {cout << p.second << " "; }); 218 | 219 | cout << "\n\n"; 220 | 221 | cout << "Number no primes: " << distance(futResults.begin(), prIt) << endl; 222 | cout << "No primes: " << endl; 223 | for_each(futResults.begin(), prIt, [](auto p) {cout << p.second << " "; }); 224 | 225 | cout << endl; 226 | 227 | } 228 | ``` 229 | 230 | 示例的基本思想是,客户端可以在激活列表上并发地安排作业。线程的工作是确定哪些数是质数。激活列表是活动对象的一部分,而活动对象在一个单独的线程上进行入队操作,并且客户端可以在激活列表中查询作业的结果。 231 | 232 | 程序的详情:5个客户端通过`getFutures`将工作(第121 - 126行)入队到`activeObject`。`numberPrimes`中的数字是1000000到1000000000之间(第96行)的随机数,将这些数值放入`vector> `中。`future`持有一个`bool`和`int`对,其中`bool`表示`int`值是否是质数。再看看第108行:`future .push_back(activeObject.enqueueTask(numb))`。此调用将触发新作业进入激活列表的队列,所有对激活列表的调用都必须受到保护,这里激活列表是一个promise队列(第89行):`deque(int)>, int >> `。 233 | 234 | 每个promise在调用执行函数对象`IsPrime`(第39 - 47行)时,会返回一个`bool`和`int`对。现在,工作包已经准备好了,开始计算吧。所有客户端在第129 - 133行中返回关联future的句柄,并把所有的future放在一起(第136 - 146行),这样会使工作更加容易。第149行中的调用`activeObject.run()`启动执行。`run`(第64 - 72行)启动单独的线程,并执行promises(第68行),直到执行完所有作业(第66行)。`isEmpty`(第83 - 87行)确定队列是否为空,`dequeTask`会返回一个新任务。通过在每个`future`上调用`futResults.push_back(fut.get())`(第154行),所有结果都会推送到`futResults`上。第156行对成对的向量进行排序:`vector>`。其余代码则是给出了计算结果,第159行中的迭代器`prIt`将第一个迭代器指向一个素数对。 235 | 236 | 程序打印素数数量为`distance(prIt, futResults.end())`(第162行),并(第164行)逐一显示。 237 | 238 | ![](../../../images/Patterns/Concurrent-Architecture/3.png) 239 | 240 | ## 拓展阅读 241 | 242 | * [Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects](https://www.dre.vanderbilt.edu/~schmidt/POSA/POSA2/) 243 | * [Prefer Using Active Object instead of Naked Thread (Herb Sutter)]( http://www.drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/225700095) 244 | * [Active Object implementation in C++11]( https://github.com/lightful/syscpp/) 245 | 246 | -------------------------------------------------------------------------------- /content/Pattrns/Concurrent-Architecture/8.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 监控对象 2 | 3 | 监控对象模式会同步并发执行,以确保对象只执行一个方法。并且,还允许对象的方法协同调度执行序列。这种模式也称为线程安全的被动对象模式。 4 | 5 | ## 模式要求 6 | 7 | 多个线程同时访问一个共享对象时,需要满足以下要求: 8 | 9 | 1. 并发访问时,需要保护共享对象不受非同步读写操作的影响,以避免数据争用。 10 | 2. 必要的同步是实现的一部分,而不是接口的一部分。 11 | 3. 当线程处理完共享对象时,需要发送一个通知,以便下一个线程可以使用共享对象。这种机制有助于避免死锁,并提高系统的整体性能。 12 | 4. 方法执行后,共享对象的不变量必须保持不变。 13 | 14 | 客户端(线程)可以访问监控对象的同步方法。因为监控锁在任何时间点上,只能运行一个同步方法。每个监控对象都有一个通知等待客户端的监控条件。 15 | 16 | ## 组件 17 | 18 | 监控对象由四个组件组成。 19 | 20 | ![](../../../images/Patterns/Concurrent-Architecture/4.png) 21 | 22 | 1. 监控对象:支持一个或多个方法。每个客户端必须通过这些方法访问对象,每个方法都必须在客户端线程中运行。 23 | 2. 同步方法:监控对象支持同步方法。任何给定的时间点上,只能执行一个方法。线程安全接口有助于区分接口方法(同步方法)和(监控对象的)实现方法。 24 | 3. 监控锁:每个监控对象有一个监控锁,锁可以确保在任何时间点上,只有一个客户端可以访问监控对象。 25 | 4. 监控条件:允许线程在监控对象上进行调度。当前客户端完成同步方法的调用后,下一个等待的客户端将被唤醒。 26 | 27 | 虽然监控锁可以确保同步方法的独占访问,但是监控条件可以保证客户端的等待时间最少。实质上,监控锁可以避免数据竞争,条件监控可以避免死锁。 28 | 29 | ## 运行时行为 30 | 31 | 监控对象及其组件之间的交互具有不同的阶段。 32 | 33 | * 当客户端调用监控对象的同步方法时,必须锁定全局监控锁。如果客户端成功访问,将执行同步方法,并在结束时解锁。如果客户端访问不成功,则阻塞客户端,进入等待状态。 34 | * 当客户端阻塞时,监控对象会在解锁时,对阻塞的客户端发送通知。通常,等待是资源友好的休眠,而不是忙等。 35 | * 当客户端收到通知时,会锁定监控锁,并执行同步方法。同步方法结束时解锁,并发送监控条件的通知,以通知下一个客户端去执行。 36 | 37 | ## 优点和缺点 38 | 39 | 监控对象的优点和缺点是什么? 40 | 41 | * 优点: 42 | * 同步方法会完全封装在实现中,所以客户端不知道监控对象会隐式同步。 43 | * 同步方法将自动调度监控条件的通知/等待机制,其表现类似一个简单的调度器。 44 | * 缺点: 45 | * 功能和同步是强耦合的,所以很难改变同步机制。 46 | * 当同步方法直接或间接调用同一监控对象时,可能会发生死锁。 47 | 48 | 下面的程序段中定义了一个ThreadSafeQueue。 49 | 50 | ```c++ 51 | // monitorObject.cpp 52 | 53 | #include 54 | #include 55 | #include 56 | #include 57 | #include 58 | #include 59 | #include 60 | 61 | template 62 | class Monitor { 63 | public: 64 | void lock() const { 65 | monitMutex.lock(); 66 | } 67 | void unlock() const { 68 | monitMutex.unlock(); 69 | } 70 | 71 | void notify_one() const noexcept { 72 | monitCond.notify_one(); 73 | } 74 | void wait() const { 75 | std::unique_lock monitLock(monitMutex); 76 | monitCond.wait(monitLock); 77 | } 78 | 79 | private: 80 | mutable std::recursive_mutex monitMutex; 81 | mutable std::condition_variable_any monitCond; 82 | }; 83 | 84 | template 85 | class ThreadSafeQueue : public Monitor> { 86 | public: 87 | void add(T val) { 88 | derived.lock(); 89 | myQueue.push(val); 90 | derived.unlock(); 91 | derived.notify_one(); 92 | } 93 | 94 | T get() { 95 | derived.lock(); 96 | while (myQueue.empty()) derived.wait(); 97 | auto val = myQueue.front(); 98 | myQueue.pop(); 99 | derived.unlock(); 100 | return val; 101 | } 102 | private: 103 | std::queue myQueue; 104 | ThreadSafeQueue& derived = static_cast&>(*this); 105 | }; 106 | 107 | class Dice { 108 | public: 109 | int operator()() { return rand(); } 110 | private: 111 | std::functionrand = std::bind(std::uniform_int_distribution<>(1, 6), 112 | std::default_random_engine()); 113 | }; 114 | 115 | 116 | int main() { 117 | 118 | std::cout << std::endl; 119 | 120 | constexpr auto NUM = 100; 121 | 122 | ThreadSafeQueue safeQueue; 123 | auto addLambda = [&safeQueue](int val) {safeQueue.add(val); }; 124 | auto getLambda = [&safeQueue] {std::cout << safeQueue.get() << " " 125 | << std::this_thread::get_id() << ";"; 126 | }; 127 | 128 | std::vector addThreads(NUM); 129 | Dice dice; 130 | for (auto& thr : addThreads) thr = std::thread(addLambda, dice()); 131 | 132 | std::vector getThreads(NUM); 133 | for (auto& thr : getThreads) thr = std::thread(getLambda); 134 | 135 | for (auto& thr : addThreads) thr.join(); 136 | for (auto& thr : addThreads) thr.join(); 137 | 138 | std::cout << "\n\n"; 139 | 140 | } 141 | ``` 142 | 143 | 该示例的核心思想是,将监控对象封装在一个类中,这样就可以重用。监控类使用`std::recursive_mutex`作为监控锁,`std::condition_variable_any`作为监控条件。与`std::condition_variable`不同,`std::condition_variable_any`能够接受递归互斥。这两个成员变量都声明为可变,因此可以在常量方法中使用。监控类提供了监控对象的最小支持接口。 144 | 145 | 第34 - 55行中的`ThreadSafeQueue`使用线程安全接口扩展了第53行中的`std::queue`。`ThreadSafeQueue`继承于监控类,并使用父类的方法来支持同步的方法`add`和`get`。方法`add`和`get`使用监控锁来保护监控对象,特别是非线程安全的`myQueue`。当一个新项添加到`myQueue`时,`add`会通知等待线程,并且这个通知是线程安全的。当如`ThreadSafeQueue`这样的模板类,将派生类作为基类的模板参数时,这属于C++的一种习惯性用法,称为CRTP:`class ThreadSafeQueue: public Monitor>`。理解这个习惯的关键是第54行:`ThreadSafeQueue& derived = static_cast&>(*this)`,该表达式将`this`指针向下转换为派生类。监控对象`safeQueue`第72行使用(第73行和第74行中的)Lambda函数添加一个数字,或从同步的`safeQueue`中删除一个数字。`ThreadSafeQueue`本身是一个模板类,可以保存任意类型的值。程序模拟的是100个客户端向`safeQueue`添加100个介于1 - 6之间的随机数(第78行)的同时,另外100个客户端从`safeQueue`中删除这100个数字。程序会显示使用的线程的编号和id。 146 | 147 | ![](../../../images/Patterns/Concurrent-Architecture/5.png) 148 | 149 | > 奇异递归模板模式(CRTP) 150 | > 151 | > 奇异递归模板模式,简单地说,CRTP代表C++中的一种习惯用法,在这种用法中,Derived类派生自类模板Base,因此Base作为Derived模板参数。 152 | > 153 | > ```c++ 154 | >template 155 | > class Base{ 156 | > .... 157 | > }; 158 | > 159 | > class Derived : public Base{ 160 | > .... 161 | > }; 162 | > ``` 163 | > 164 | > 理解CRTP习惯用法的关键是,实例化方法是惰性的,只有在需要时才实例化方法。CRTP有两个主要的用例。 165 | > 166 | > * 静态多态性:静态多态性与动态多态性类似,但与使用虚方法的动态多态性相反,方法调用的分派在编译时进行。 167 | >* Mixin: Mixin是设计混合代码类时的一个流行概念。`ThreadSafeQueue`使用Mixin技术来扩展它的接口。通过从`Monitor`类派生`ThreadSafeQueue`,派生类`ThreadSafeQueue`获得类`Monitor`的所有方法:`ThreadSafeQueue: public Monitor>`类。 168 | > 169 | >[惰性C++:CRTP]( https://www.modernescpp.com/index.php/c-is-still-lazy)一文中,有对CRTP习语有更深入地描述。 170 | 171 | 活动对象和监控对象在几个重要的方面类似,但也有不同。这两种体系结构模式,会同步对共享对象的访问。活动对象的方法在不同线程中执行,而监控对象的方法则在同一线程中执行。活动对象更好地将其方法调用与执行解耦,因此更容易维护。 172 | 173 | ## 扩展阅读 174 | 175 | * [Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects](https://www.dre.vanderbilt.edu/~schmidt/POSA/POSA2/) -------------------------------------------------------------------------------- /content/Pattrns/Concurrent-Architecture/8.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 半同步/半异步 2 | 3 | 半同步/半异步模式会对并发系统中异步和同步服务进行解耦,从而在不过度降低性能的情况下简化编程。该模式引入了两个可以相互通信的层,一个用于异步,另一个用于同步。 4 | 5 | ![](../../../images/Patterns/Concurrent-Architecture/6.png) 6 | 7 | 半同步/半异步模式通常用于服务器的事件循环或图形界面。事件循环的工作流是将事件请求插入队,并在单独的线程中同步处理。异步处理确保了运行效率,而同步处理简化了申请流程。异步服务层和同步服务层分解为两个层,并且在这两个层之间有队列坐标。异步层由较底层的系统服务(如中断)组成,而同步层由较高层的服务(如数据库查询或文件操作)组成。异步层和同步层可以通过队列层相互通信。 8 | 9 | ## 优点和缺点 10 | 11 | 半同步/半异步模式的优点和缺点是什么? 12 | 13 | * 优点: 14 | * 异步和同步分界线很明确。底层系统服务在异步层中处理,高层服务在同步层中处理。 15 | * 对请求队列处理的层,保证了异步层和同步层的解耦。 16 | * 清晰的分离使软件更容易理解、调试、维护和扩展。 17 | * 同步服务中的阻塞不会影响异步服务。 18 | * 缺点: 19 | * 异步层和同步层之间交叉的部分可能会导致开销。通常,因为异步服务通常在内核空间中运行,同步服务在用户空间中运行,所以“边界的部分”会涉及内核空间和用户空间之间的上下文切换。 20 | * 为了严格分离各层,要求复制数据或数据是不可变的 21 | 22 | 半同步/半异步模式通常用于事件的多路分解和调度框架,如Reactor或Proactor模式。 23 | 24 | ## Reactor模式 25 | 26 | Reactor模式也称为调度程序或通知程序。该模式是一个事件驱动的框架,用于将多个服务请求并发地分发到各个服务端。 27 | 28 | **使用要求** 29 | 30 | 服务器应该并发地处理客户端的请求。每个客户端的请求都有一个唯一标识符,并支持映射到特定的服务端。以下几点是Reactor必备的: 31 | 32 | * 不阻塞。 33 | * 支持最大吞吐量,避免不必要的上下文切换,避免数据的复制或同步。 34 | * 易于扩展,以支持服务的修改。 35 | * 不使用复杂的同步机制。 36 | 37 | **解决方案** 38 | 39 | 对于支持的服务类型,实现一个事件处理程序来满足特定客户端的请求。反应器中使用注册的方式,将服务端的事件处理程序进行注册,这里使用了事件解复用器来同步等待所有传入的事件。当一个事件到达时,反应器得到通知,并将相应的事件分派给特定的服务。 40 | 41 | **组件** 42 | 43 | ![](../../../images/Patterns/Concurrent-Architecture/7.png) 44 | 45 | * 句柄: 46 | * 句柄标识了事件源,如网络连接、打开文件或GUI事件。 47 | * 事件源生成连接、读或写等事件,这些事件会在句柄上进行排队。 48 | * 同步事件多路分解器: 49 | * 同步事件多路分解器会等待一个或多个事件。多路分解器会进行阻塞,直到关联的句柄能够处理该事件为止。 50 | * 事件处理接口: 51 | * 事件处理程序定义了处理特定事件的接口。 52 | * 事件处理程序定义了应用程序支持的服务。 53 | * 特定事件处理程序: 54 | * 特定的事件处理实现,由事件处理接口确定。 55 | * 反应器: 56 | * 反应器支持接口注册和注销。 57 | * 反应器使用同步事件多路分解器,例如系统调用[select](https://en.wikipedia.org/wiki/Select_(Unix)), [epoll](https://en.wikipedia.org/wiki/Epoll)或[WaitForMultipleObjects]( https://docs.microsoft.com/en-us/windows/desktop/api/synchapi/nf-synchapi-waitformultipleobjects)来等待特定事件。 58 | * 反应器将事件映射到具体处理程序上。 59 | * 反应器会对事件循环的生命周期进行管理。 60 | 61 | 反应器(而不是应用程序)等待特定事件,并进行分解和分派。具体的事件处理在反应器中注册,反应器改变了控制流程。反应器等待特定事件,并调用特定的处理程序。这种控制的倒置,称为[好莱坞原则](https://en.wikipedia.org/wiki/Inversion_of_control)。(译者注:“不要给[我们](https://baike.baidu.com/item/我们/2751)打电话,我们会给你打电话(don‘t call us, we‘ll call you)”这是著名的好莱坞原则。) 62 | 63 | 下面的代码段显示了C++框架的事件循环——[自适应通信环境(ACE)]( https://www.dre.vanderbilt.edu/~schmidt/ACE.html)。 64 | 65 | ```c++ 66 | // CTRL c 67 | SignalHandler *mutateTimer1 = new SignalHandler(timerId1); 68 | 69 | // CTRL z 70 | SignalHandler *mutateTimer2 = new SignalHandler(timerId2); 71 | 72 | ACE_Reactor::instance()->register_handler(SIGINT, mutateTimer1); 73 | ACE_Reactor::instance()->register_handler(SIGTSTP, mutateTimer2); 74 | 75 | 76 | // "run" the timer. 77 | Timer::instance()->wait_fot_event(); 78 | ``` 79 | 80 | 第2行和第5行定义按CTRL+c和CTRL+z的键盘事件的信号处理程序。第7行和第8行记录它们,事件循环从第12行开始。 81 | 82 | **优点和缺点** 83 | 84 | 反应器模式的优点和缺点是什么呢? 85 | 86 | * 优点: 87 | * 框架和应用逻辑解耦。 88 | * 各种具体处理程序的模块化。 89 | * 接口和实现的分离,使服务更容易适应或扩展。 90 | * 整体结构支持并发。 91 | * 缺点: 92 | * 需要调用事件分解系统。 93 | * 长时间运行的程序会阻塞反应器。 94 | * 反转控制使得测试和调试更加困难。 95 | 96 | 半同步/半异步模式通常在反应器模式中,用于在独立线程中对客户端请求的响应。 97 | 98 | Proactor模式是反应器模式的异步变体。反应器模式同步地分解和分派事件处理程序,而Proactor模式异步地分派事件处理程序。 99 | 100 | ## Proactor模式 101 | 102 | Proactor模式允许事件驱动的应用程序,对异步操作完成时触发的服务请求进行多路的分解和分派。 103 | 104 | **使用要求** 105 | 106 | 事件驱动程序(如服务器),其性能可以通过异步处理服务来提高。为了实现这种方式,事件驱动程序必须同步处理多个事件,从而避免昂贵的数据同步或上下文切换。此外,修改后的服务应该很容易集成入系统,应用程序应该避免对多线程和同步方式进行挑战。 107 | 108 | **解决方案** 109 | 110 | 将服务分为两部分:异步运行的长时间操作和处理操作结果的程序。结果处理程序与反应器模式中的事件处理程序非常相似,不过异步操作通常是操作系统的工作。所以,作为反应器模式,Proactor模式定义了事件循环。 111 | 112 | 异步操作(如连接请求)是该模式的独特之处,并且在不阻塞调用线程的情况下执行操作。当耗时相当长的操作完成时,它将一个完成事件放入完成事件队列,Proactor通过使用异步事件多路分解器在队列上等待。异步事件多路分解器将从队列中删除完成事件,而Proactor将其分派给特定的处理程序,处理操作的结果。 113 | 114 | **组件** 115 | 116 | Proactor模式由九个组件组成。 117 | 118 | ![](../../../images/Patterns/Concurrent-Architecture/8.png) 119 | 120 | * 句柄: 121 | * 表示操作系统的实体(如套接字),可以生成完成事件。 122 | * 异步操作: 123 | * 通常异步执行耗时相当长的操作。可以在套接字上进行读或写操作。 124 | * 异步操作处理器: 125 | * 执行异步操作,完成后在完成事件队列上注册完成事件。 126 | * 完成事件接口: 127 | * 定义处理异步操作结果的接口。 128 | * 完成事件处理逻辑: 129 | * 用特定的程序处理异步操作的结果。 130 | * 完成事件队列: 131 | * 作为完成事件的缓冲,直到被异步事件分解器移出队列。 132 | * 异步事件多路分解器: 133 | * 在完成事件队列上等待完成事件时,可以阻塞程序。 134 | * 从完成事件队列中删除完成事件。 135 | * Proactor: 136 | * 调用异步事件分解器对完成事件进行脱队操作。 137 | * 分解和分派完成事件,并调用特定的处理程序处理完成事件。 138 | * 创建者: 139 | * 调用异步操作。 140 | * 可与异步操作处理器进行交互。 141 | 142 | **优点和缺点** 143 | 144 | Proactor模式的优点和缺点是什么呢? 145 | 146 | * 优点: 147 | * 应用程序将独立的异步功能进行功能性分离。 148 | * Proactor的接口可用于支持不同操作系统上的多种异步事件分解器。 149 | * 应用程序不需要启动新线程,因为耗时相当长的异步操作会在调用者的线程中运行。 150 | * Proactor模式可以避免上下文的切换。 151 | * 应用程序的逻辑部分不启动任何线程,因此不需要同步。 152 | * 缺点: 153 | * 为了高效地应用Proactor模式,操作系统需要支持异步操作。 154 | * 由于操作启动和完成之间在时间和空间上的分离,调试或测试程序相当困难。 155 | * 异步操作的调用和完成事件的维护需要额外的内存。 156 | 157 | > **Asio,即「异步 IO」(Asynchronous Input/Output)** 158 | > 159 | > 随着[Boost.Asio](https://www.boost.org/doc/libs/1_69_0/doc/html/boost_asio.html)库可能作为网络库成为C++23的一部分,在未来大家可以在C++中轻易实现Proactor模式了。Boost.Asio是由Christopher Kohlhoff的提供,是“一个用于网络和低级I/O编程的跨平台C++库,并使用现代C++为其他开发者提供了一致性异步模型”。 160 | 161 | ## 扩展阅读 162 | 163 | * [Adaptive Communication Environment (ACE)]( https://en.wikipedia.org/wiki/Adaptive_Communication_Environment) 164 | * [Boost.Asio]( https://www.boost.org/doc/libs/1_69_0/doc/html/boost_asio.html) 165 | * [Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects]( https://www.dre.vanderbilt.edu/~schmidt/POSA/POSA2/) 166 | * [基于 Asio 的 C++ 网络编程](https://segmentfault.com/a/1190000007225464) 167 | 168 | -------------------------------------------------------------------------------- /content/Pattrns/Patterns-and-Best-Practices/6.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 模式和最佳实践 2 | 3 | 本章的目标是了解模式是什么,以及模式有什么好处。我的实用主义观点有些非正式的,并且还戴了C++的眼镜。为了更全面地讨论这个主题,我会提供一些文献的链接,供大家进一步的了解细节。 4 | 5 | 首先,什么是模式? 6 | 7 | 用[Christopher Alexander](https://en.wikipedia.org/wiki/Christopher_Alexander)的话来说,“每个模式都是由三部分组成其规则,它描述了特定的上下文、问题和解决方案之间的关系。“ 8 | 9 | 更通俗地说,模式是对特定(文档完善的)解决方案的设计挑战。 -------------------------------------------------------------------------------- /content/Pattrns/Patterns-and-Best-Practices/6.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 相关历史 2 | 3 | Cristopher Alexander是模式之父,他的模式本质是以人为本的设计,其中是关于城镇、房屋和建筑物的设计。总的来说,这中方式对软件设计有很大的影响。1994年,“风尘四侠”(Eric Gamma、Richard Helm、Ralph Johnson和John Vlissides)出版了他们的书[《Design Patterns: Elements of Reusable Object-Oriented Software》](https://en.wikipedia.org/wiki/Design_Patterns)。这本书包括23个针对面向对象软件设计的模式,这些模式可分为三类:创造型、结构型和行为型。书中定义了软件行业的用语,以下是一些最著名的设计模式: 4 | 5 | * Creational 创建型模式 6 | * Factory method pattern 工厂模式 7 | * Singleton pattern 单例模式 8 | * Structural 结构型模式 9 | * Adapter 适配器模式 10 | * Bridge 桥接模式 11 | * Composite 组合模式 12 | * Decorator 装饰器模式 13 | * Facade 外观模式 14 | * Proxy 代理模式 15 | * Behavioural 行为型模式 16 | * Command 命令模式 17 | * Iterator 迭代器模式 18 | * Observer 观察者模式 19 | * Strategy 策略模式 20 | * Template method 模板模式 21 | * Visitor 访问者模式 22 | 23 | 一年后,Frank Buschmann、Regine Meunier、Hans Rohnert、Peter Sommerlad和Michael Stal出版了他们非常有影响力的 [《Pattern-Oriented Software-Architecture: A System of Patterns》](https://www.wiley.com/WileyCDA/Section/id-406899.html)一书,简写为POSA,这本书是系列五部曲的第一部。它在1995年出版,有三类模式:架构模式、设计模式和习惯用法。许多模式都是现在的通用术语: 24 | 25 | * Architectural Patterns 架构模式 26 | * Layers 层模式 27 | * Pipes and Filters 管道和过滤器模式 28 | * Broker 经纪人模式 29 | * Model-View-Controller 模型-视图-控制器模式 30 | * Design Patterns 设计模式 31 | * Master-Slave 主从模式 32 | * Publish-Subscriber 发布和订阅者模式 33 | * Idioms 习惯用法 34 | * Counted-Pointer 计数器模式 35 | 36 | 这三个类别有什么区别呢?体系结构模式的重点是整个软件系统,比定义相互作用的设计模式更加抽象。习惯用法是特定编程语言中体系结构或设计模式的实现,也是三者中抽象层次最低的。 37 | 38 | POSA系列的第2卷至第5卷都有不同的关注点。他们处理并发”模式和网络对象”(第2卷),“资源管理模式”(第3卷),“分布式计算的模式语言”(第4卷),“模式和模式语言”(第5卷)。本书的同步模式和并行架构部分,深受系列的第2卷的影响。 -------------------------------------------------------------------------------- /content/Pattrns/Patterns-and-Best-Practices/6.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 价值所在 2 | 3 | 模式通常为软件开发增加了价值,这对于并发性尤为适用。附加值可以归结为三点:良好的术语,改进的文档,学习的榜样。 4 | 5 | 良好的术语意味着,软件开发人员可以使用通用且明确的词汇表,这样误解或冗长的解释都是成为过去式。如果一个软件开发人员询问,如何实现在运行时对类似的算法簇进行交换时,答案可能很简单:使用策略模式。如果软件开发人员知道策略模式,就可以立即考虑如何使用策略模式;如果没有,他就需要查阅文献。 6 | 7 | 文档在两个方面可以改进。首先,关于软件系统的文档,可以进行图形化或文本化,因为在文档中读到“使用了观察者模式”,就知道系统有一种主题/观察者结构。这意味着观察员将登记或注销,如有必要则会向所有观察员发出通知。第二,对具体实现的了解,这样就可以直接跳到源代码并搜索关键字,如observer、subject或notify。 8 | 9 | 模式就是向榜样学习,从最好的人那里学习已有经验,不要重复他们的错误。了解它们为哪些典型的问题提供经过已验证的解决方案,并是如何控制复杂性的。每个模式都会提供相应的信息,什么时候应该使用它,使用它的会有什么后果,以及如何实现和已知用法。 10 | 11 | -------------------------------------------------------------------------------- /content/Pattrns/Patterns-and-Best-Practices/6.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 模式与最佳实践 2 | 3 | 这个章节挺奇怪的,感觉重复,但又不重复。如果不可变值或纯函数之类的实践是模式或最佳实践,那么我经常会参与这类话题的争论。模式是文档化的最佳实践,并且我从数场"战斗"中学到了一些东西。 4 | 5 | * 这两个术语不能完全区别开来。 6 | * 如果实践是定义良好的模式,那么会我将它放到模式桶中。 7 | * 如果实践具有技巧特征,并且没有正式的结构,我将它放到最佳实践桶中。 8 | * 今天的最佳实践,可能成为明天的模式。 9 | 10 | -------------------------------------------------------------------------------- /content/Pattrns/Patterns-and-Best-Practices/6.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 反模式 2 | 3 | 如果模式代表了最佳实践,那么反模式就代表经验教训,或者用[Andrew Koenig](https://en.wikipedia.org/wiki/Andrew_Koenig_(programmer))的话来说:“对于问题的糟糕描述,导致了糟糕的解决方案。”如果仔细阅读并发模式的文献,就会看到双重检查锁定模式。双重检查锁定模式的基本思想,简言之,以优化的方式对共享状态进行线程安全初始化,这种共享状态通常是[单例](https://en.wikipedia.org/wiki/Singleton_pattern)。我将双重检查锁定模式放在本书的案例研究一章中,以明确强调:使用双重检查锁定模式可能会导致未定义行为。双重检查锁定模式的问题,本质上可以归结为单例模式的问题。 4 | 5 | 如果使用单例模式,必须考虑以下挑战: 6 | 7 | * 单例对象是一个全局对象。基于这个事实,单例的使用在(大多数情况下)接口中是不可见的。其结果是在使用单例的代码中隐藏了一个依赖项。 8 | * 单例对象是静态的,因此一旦创建就不会被销毁。它的生命周期和程序的生命周期相同。 9 | * 如果类的静态成员(如单例)依赖于在另一个单元中定义的静态成员,则不能保证先初始化哪个静态成员,那么每个静态成员初始化失败的概率是50%。 10 | * 当类的实例可以完成任务时,通常会使用单例。许多开发者使用单例来证明自己了解设计模式。 11 | 12 | -------------------------------------------------------------------------------- /content/Pattrns/Synchronisation-Patterns/7.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 同步模式 2 | 3 | 处理并发时,尤其要注意共享变量、可变状态或[Tony Van Eerd](https://github.com/tvaneerd)(在CppCon 2014)提及的“无锁示例”:“你需要忘记在幼儿园学到的那点玩意儿(即:阻止共享)”。 4 | 5 | ![](../../../images/Patterns/Synchronisation-Patterns/1.png) 6 | 7 | 共享数据特别容易产生竞争。如果是仅处理共享或突变,则不会发生数据竞争。这正是本章的两个重点:处理共享和处理突变。 8 | 9 | -------------------------------------------------------------------------------- /content/Pattrns/Synchronisation-Patterns/7.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 处理共享 2 | 3 | 如果使用不共享数据,就没有竞争。不共享意味着线程只处理本地变量,可以通过值复制、特定的线程存储,也可以通过受保护的数据通道将结果传输到future来实现。本节中的模式非常直观,我会给出一些简单的解释。 4 | 5 | ## 值复制 6 | 7 | 线程通过值复制,而不是引用来获取参数时,就不需要对任何数据的访问进行同步,也就没有数据竞争的条件和数据生命周期的问题。 8 | 9 | **使用引用的数据竞争** 10 | 11 | 下面的程序启动三个线程:一个线程通过复制获取参数,另一个线程通过引用获取参数,最后一个线程通过常量引用获取参数。 12 | 13 | ```c++ 14 | // copiedValueDataRace.cpp 15 | 16 | #include 17 | #include 18 | #include 19 | #include 20 | 21 | using namespace std::chrono_literals; 22 | 23 | void byCopy(bool b) { 24 | std::this_thread::sleep_for(1ms); 25 | std::cout << "byCopy: " << b << std::endl; 26 | } 27 | 28 | void byReference(bool& b) { 29 | std::this_thread::sleep_for(1ms); 30 | std::cout << "byReference: " << b << std::endl; 31 | } 32 | 33 | void byConstReference(const bool& b) { 34 | std::this_thread::sleep_for(1ms); 35 | std::cout << "byConstReference: " << b << std::endl; 36 | } 37 | 38 | int main() { 39 | 40 | std::cout << std::boolalpha << std::endl; 41 | 42 | bool shared(false); 43 | 44 | std::thread t1(byCopy, shared); 45 | std::thread t2(byReference, std::ref(shared)); 46 | std::thread t3(byConstReference, std::cref(shared)); 47 | 48 | shared = true; 49 | 50 | t1.join(); 51 | t2.join(); 52 | t3.join(); 53 | 54 | std::cout << std::endl; 55 | 56 | } 57 | ``` 58 | 59 | 每个线程在显示布尔值之前会休眠1毫秒(第11、16和21行),其中只有线程`t1`具有布尔值的副本,因此没有数据竞争。程序显示线程`t2`和`t3`中的布尔值,而且布尔值在没有同步的情况下进行修改。 60 | 61 | ![](../../../images/Patterns/Synchronisation-Patterns/2.png) 62 | 63 | copiedValueDataRace.cpp例子中,我做了一个假设,这个假设对于布尔值来说很简单,但是对于更复杂的类型来说就不一定了。如果参数是“值对象”,那么通过复制传递参数必然就是无数据竞争。 64 | 65 | > **值对象** 66 | > 67 | > “值对象”是一个对象,相等性基于状态。值对象是不可变的,以便在创建为“相等”的情况下,保持同等的生命周期。如果通过复制将值对象传递给线程,则不需要同步访问。[ValueObject](https://martinfowler.com/bliki/ValueObject.html)源于Martin Fowler的文章,“考虑两类对象:值对象和引用对象”。 68 | 69 | **当引用为拷贝时** 70 | 71 | 示例copyedValueDataRace.cpp中的线程`t3`可能可以替换为`std::thread t3(byConstReference, shared)`。 该程序可以编译并运行,但是只是看起来像是引用而已, 原因是[`std::decay`](https://en.cppreference.com/w/cpp/types/decay)会应用于线程的每个参数。 `std::decay`对类型T的执行是从左值到右值,数组到指针和函数到指针的隐式转换。这种用例中,对类型T使用的是`[std :: remove_reference]` 。 72 | 73 | perConstReference.cpp使用不可复制类型NonCopyableClass。 74 | 75 | 线程引用参数的"隐式"复制 76 | 77 | ```c++ 78 | // perConstReference.cpp 79 | 80 | #include 81 | 82 | class NonCopyableClass { 83 | public: 84 | 85 | // the compiler generated default constructor 86 | NonCopyableClass() = default; 87 | 88 | // disallow copying 89 | NonCopyableClass& operator=(const NonCopyableClass&) = delete; 90 | NonCopyableClass(const NonCopyableClass&) = delete; 91 | 92 | }; 93 | 94 | void perConstReference(const NonCopyableClass& nonCopy){} 95 | 96 | int main() { 97 | 98 | NonCopyableClass nonCopy; 99 | 100 | perConstReference(nonCopy); 101 | 102 | std::thread t(perConstReference, nonCopy); 103 | t.join(); 104 | } 105 | ``` 106 | 107 | 对象`nonCopy`(第21行)是不可复制的, 如果使用参数`nonCopy`调用函数`perConstReference`则没什么问题,因为该函数接受常量引用参数。线程`t`(第25行)中使用相同的函数,会导致GCC 6生成300多行冗长的编译器错误: 108 | 109 | ![](../../../images/Patterns/Synchronisation-Patterns/3.png) 110 | 111 | 因为复制构造函数在NonCopyableClass类中是不可用的,所以错误消息的重要部分位于屏幕截图中间的红色部分:“错误:使用已删除的功能”。 112 | 113 | ![](../../../images/Patterns/Synchronisation-Patterns/4.png) 114 | 115 | **引用参数的生命周期问题** 116 | 117 | 如果分离通过引用获取参数的线程,则必须格外小心。 copyValueValueLifetimeIssues.cpp中就有未定义行为。 118 | 119 | 使用引用引发的生命周期问题 120 | 121 | ```c++ 122 | // copiedValueLifetimeIssues.cpp 123 | 124 | #include 125 | #include 126 | #include 127 | 128 | void executeTwoThreads() { 129 | 130 | const std::string localString("local string"); 131 | 132 | std::thread t1([localString] { 133 | std::cout << "Per Copy: " << localString << std::endl; 134 | }); 135 | 136 | std::thread t2([&localString] { 137 | std::cout << "Per Reference: " << localString << std::endl; 138 | }); 139 | 140 | t1.detach(); 141 | t2.detach(); 142 | } 143 | 144 | using namespace std::chrono_literals; 145 | 146 | int main() { 147 | 148 | std::cout << std::endl; 149 | 150 | executeTwoThreads(); 151 | 152 | std::this_thread::sleep_for(1s); 153 | 154 | std::cout << std::endl; 155 | 156 | } 157 | ``` 158 | 159 | executeTwoThreads(第7 - 21行)启动了两个线程,且两个线程都被分离(第19行和第20行),并且线程在执行时会打印局部变量`localString`(第9行)。第一个线程通过复制捕获局部变量,第二个线程通过引用捕获局部变量。为了让程序看起来简单,我使用Lambda函数来绑定参数。 160 | 161 | 因为executeTwoThreads函数不会等待两个线程完成,所以线程`t2`引用本地字符串,而该字符串与函数的生命周期绑定,这就会导致未定义行为的发生。奇怪的是,在GCC 6中以最大优化`-O3`编译连接的可执行文件似乎可以工作,而非优化的可执行文件却崩溃了。 162 | 163 | ![](../../../images/Patterns/Synchronisation-Patterns/5.png) 164 | 165 | **扩展阅读** 166 | 167 | * [Pattern-Oriented Software Architecture: A Pattern Language for Distributed Computing](http://www.dre.vanderbilt.edu/~schmidt/POSA/POSA4/) 168 | 169 | ## 线程特定的存储器 170 | 171 | 线程的本地存储,允许多个线程通过全局访问使用本地存储。通过使用存储说明符`thread_local`,变量变成了线程的局部变量。这意味着,可以在不同步的情况下,使用线程局部变量。 172 | 173 | 下面是一个典型的用例。假设想要计算一个向量`randValues`的元素和,使用for循环执行此任务非常简单。 174 | 175 | ```c++ 176 | // calculateWithLoop.cpp 177 | ... 178 | unsigned long long sum = {}; 179 | for (auto n: randValues) sum += n; 180 | ``` 181 | 182 | 不过,电脑有四个核心,也可以使串行程序变成一个并发程序。 183 | 184 | ```c++ 185 | // threadLocalSummation.cpp 186 | ... 187 | thread_local unsigned long long tmpSum = 0; 188 | void sumUp(std::atomic& sum, const std::vector& val, 189 | unsigned long long beg, unsigned long long end){ 190 | for (auto i = beg; i < end; ++i){ 191 | tmpSum += val[i]; 192 | } 193 | sum.fetch_add(tmpSum, std::memory_order_relaxed); 194 | } 195 | ... 196 | std::atomic sum{}; 197 | std::thread t1(sumUp, std::ref(sum), std::ref(randValues), 0, fir); 198 | std::thread t2(sumUp, std::ref(sum), std::ref(randValues), fir, sec); 199 | std::thread t3(sumUp, std::ref(sum), std::ref(randValues), sec, thi); 200 | std::thread t4(sumUp, std::ref(sum), std::ref(randValues), thi, fou); 201 | ``` 202 | 203 | 将for循环放入函数中,让每个线程计算线程局部变量`tmpSum`中总和的四分之一。`sum.fetch_add(tmpSum, std::memory_order_relaxed)`最后以原子的方式汇总所有值。 204 | 205 | > **使用标准模板库的算法** 206 | > 207 | > 如果有算法标准模板库可以做这项工作,就用不着循环了。本例中,[std::accumulate](https://en.cppreference.com/w/cpp/algorithm/accumulate)就可以完成这项工作,以汇总向量加和:`sum = std::accumulate(randValues.begin(), randValues.end(), 0) `。在C++17中,可以使用`std::accumulate`的并行版本`std::reduce`:`sum = std::reduce(std::execution::par, randValues.begin(), randValues.end(), 0)`。 208 | 209 | **扩展阅读** 210 | 211 | * [ValueObject](https://martinfowler.com/bliki/ValueObject.html) 212 | * [Pattern-Oriented Software Architecture: Patterns for Concurrent and Networked Objects](https://www.dre.vanderbilt.edu/~schmidt/POSA/POSA2/) 213 | 214 | ## Future 215 | 216 | C++11提供了三种类型的future和promise:`std::async`、`std::packaged_task`和`std::promise`与`std::future`对。promise这个词可以追溯到70年代。future是可写promise设置的只读占位符。从同步的角度来看,promise/future对的关键属性是两者都由受保护的数据通道进行连接。 217 | 218 | 实现future时需要做出一些决策: 219 | 220 | * future可以通过get调用隐式或显式地获取值。 221 | * future可以积极地或消极地启动计算,只有`std::async`可以通过启动策略控制是否支持延迟计算。 222 | 223 | ```c++ 224 | auto lazyOrEager = std::async([]{ return "LazyOrEager"; }); 225 | auto lazy = std::async(std::launch::deferred, []{ return "Lazy"; }); 226 | auto eager = std::async(std::launch::async, []{ return "Eager"; }); 227 | 228 | lazyOrEager.get(); 229 | lazy.get(); 230 | eager.get(); 231 | ``` 232 | 233 | 如果没有指定启动策略,则由系统决定是立即启动还是延迟启动。通过使用启动策略`std::launch::async`,创建一个新线程,promise会立即开始它的工作。这与启动策略`std::launch::async`不同,`eager.get()`会启动promise,而promise是在创建线程中执行的。 234 | 235 | * 如果promise的值不可用,则future阻塞或抛出异常。C++11阻塞了wait或get,也可以等待promise的超时(`wait_for`和`wait_until`)。 236 | * 有多种方法实现future:[协程](https://en.wikipedia.org/wiki/Coroutine)、[生成器]( https://en.wikipedia.org/wiki/Generator_(computer_programming))或[通道](https://en.wikipedia.org/wiki/Channel_(programming))。 237 | 238 | **扩展阅读** 239 | 240 | * [Futures and promises](https://en.wikipedia.org/wiki/Futures_and_promises) -------------------------------------------------------------------------------- /content/Reader-Testimonials.md: -------------------------------------------------------------------------------- 1 | ## 读者推荐 2 | 3 | “《Concurrency with Modern C++》是C++并发编程的指南。本书从C++内存模型开始,有很多经典案例的研究,大量的多线程技巧。将使您更了解并发的特性,甚至满足您的好奇心!” 4 | 5 |

— Bart Vandewoestyne:Esterline高级研发工程师

6 | ”Rainer Grimm的《Concurrency with Modern C++》是一本好书,涵盖了并发性的理论和实践,以及C++20标准的(可能)变化。并提供了关于并发实践的讨论,提供了示例代码,以加强每个主题的细节。内容丰富,值得一读!” 7 | 8 |

— Ian Reeve:戴尔软件存储高级工程师。

9 | ”阅读《Concurrency with Modern C++》是成为多线程专家最简单的方法。这本书既有简单的内容,也有进阶的主题。它包含了研发人员所需的一切:大量的理论内容和代码示例,以及出色的解释,还有易错点介绍。我很喜欢它,并强烈推荐所有人阅读。” 10 | 11 |

— Robert Badea:技术带头人

12 | ## -------------------------------------------------------------------------------- /content/Source-Code.md: -------------------------------------------------------------------------------- 1 | # 代码说明 2 | 3 | 只要有合适的编译器,就可以编译并运行所有示例源码。这里要说明一下,只有在必要时,我才在源文件中使用`using namespace std`。 4 | 5 | ## 运行程序 6 | 7 | 编译和运行本书中C++11和C++14的例子并不难。任何支持新标准的C++编译器都可以编译这些例子。[GCC]( https://gcc.gnu.org/) 和[clang]( https://clang.llvm.org/) 编译器,必须指定C++标准,以及要链接的线程库。 例如,GCC的g++编译器使用以下命令行创建一个名为thread的可执行程序: 8 | 9 | > g++ -std=c++14 -pthread thread.cpp -o thread. 10 | 11 | * -std=c++14: 使用C++14标准。 12 | * -pthread: 使用pthread库作为后端,对多线程进行支持。 13 | * thread.cpp: 源码文件。 14 | * -o thread: 可执行程序名。 15 | 16 | 同样的命令行也适用于clang++编译器。Microsoft Visual Studio 17 C++编译器也支持C++ 14。 17 | 18 | 如果没有合适的C++编译器使用,那么可以使用在线编译器。比如:Arne Mertz博客提供的[C++ Online Compiler](https://arne-mertz.de/2017/05/online-compilers)。 19 | 20 | C++ 17和C++ 20/23的故事比较复杂。我安装了[HPX (High Performance ParalleX)](http://stellar.cct.lsu.edu/projects/hpx/)框架,这是个C++通用运行时系统,适用于任何规模的并行和分布式应用。HPX已经实现了C++ 17并行的STL和C++ 20/23的许多并发特性。可参考“未来:C++ 20/23”一章中相应的内容和代码。 21 | 22 | -------------------------------------------------------------------------------- /content/The-Details/Case-Studies/4.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 案例研究 2 | 3 | 了解了内存模型和多线程接口后,现在就要进行实践了,本章会提供一些性能数据作为参考。 4 | 5 | > **电脑配置参考** 6 | > 7 | > 我用Linux桌面版(GCC 4.8.3)和Windows笔记本电脑(cl.exe 19.00.23506)对程序的性能进行测试,使用优化的64位可执行文件进行测试。Linux PC有四个核心,而Windows PC有两个核心。下面是这两个编译器的详细信息: 8 | > 9 | > ![](../../../images/detail/Case-Studies/10.png) 10 | 11 | 读者们应该只将这里的性能数值作为参考。我更喜欢凭直觉判断哪些算法可行,哪些算法不可行,但对Linux和Windows操作系统支持算法的确切数目不感兴趣。我想知道一些算法在不同的操作系统下,是否会有不同的性能表现(译者注:这里作者主要想比较操作系统中的实现,而不是对机器硬件进行比较)。 12 | 13 | -------------------------------------------------------------------------------- /content/The-Details/Case-Studies/4.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 | 使用一个简单的程序,并不断对其进行改进。首先,每一次改进都可能有更多的线程交错,x和y的可能性也会更多。其次,挑战随着每一次改进而增加。CppMem对每次的改进,提供了非常宝贵的参考。 -------------------------------------------------------------------------------- /content/The-Details/Memory-Model/1.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 内存模型 2 | 3 | (定义良好的)内存模型是多线程的基础件,包括两个方面的内容:一方面,它非常复杂,经常与我们的想法相矛盾。另一方面,它有助于我们更深入地了解多线程。 4 | 5 | 那么,”内存模型“是什么呢? -------------------------------------------------------------------------------- /content/The-Details/Memory-Model/1.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 内存模型的基础知识 2 | 3 | 从并发的角度来看,内存模型要解答两个问题: 4 | 5 | * 什么是内存位置? 6 | * 如果两个线程访问相同的内存位置,会发生什么? 7 | 8 | ## 内存位置是什么? 9 | 10 | 引用[cppreference.com](http://en.cppreference.com/w/cpp/language/memory_model)中对内存位置的定义: 11 | 12 | * 标量对象(算术类型、指针类型、枚举类型或`std::nullptr_t`), 13 | * 或非零长度的连续序列。 14 | 15 | 下面是内存位置的例子: 16 | 17 | ```c++ 18 | struct S { 19 | char a; // memory location #1 20 | int b : 5; // memory location #2 21 | int c : 11, // memory location #2 (continued) 22 | : 0, 23 | d : 8; // memory location #3 24 | int e; // memory location #4 25 | double f; // memory location #5 26 | std::string g; // several memory locations 27 | }; 28 | ``` 29 | 30 | 首先,对象`obj`由七个子对象组成,其中b、c两个位字段共享内存位置。 31 | 32 | 观察上述结构体定义,可以得到如下结论: 33 | 34 | * 每个变量都是一个对象。 35 | * 标量类型占用一个内存位置。 36 | * 相邻的位字段(b和c)具有相同的内存位置。 37 | * 变量至少占用一个内存位置。 38 | 39 | 那么,到了多线程的关键部分。 40 | 41 | ##两个线程访问相同的内存位置,会发生什么呢? 42 | 43 | 如果两个线程访问相同的内存位置(相邻位字段共享内存位置),并且至少有一个线程想要修改它,那么程序就会产生数据竞争,除非: 44 | 45 | 1. 修改操作为原子操作。 46 | 2. 访问按照某种先行(happens-before)顺序进行。 47 | 48 | 第二种情况非常有趣,同步语义(如互斥锁)可以建立了先行关系。这些先行关系基于原子建立,当然也适用于非原子操作。内存序(memory-ordering)是内存模型的关键部分,其定义了先行关系的细节。 49 | 50 | 对内存模型有了初步的认识后,再来看看C++内存模型中定义的“编程协议”。 -------------------------------------------------------------------------------- /content/The-Details/Memory-Model/1.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 编程协议 2 | 3 | 协议约定的双方为:开发者和系统。系统由生成机器码的编译器、执行机器码的处理器和存储程序状态的缓存组成。每个部分可以进行优化,例如:编译器可以使用寄存器或修改循环,处理器可以乱序执行或分支预测,缓存指令可以预取或缓冲。生成的(在好的情况下)可执行文件,可以针对硬件平台进行了优化。确切地说,这里不只有一个协议,而是一组(细粒度的)协议。换句话说:遵循越弱的规则,程序的优化潜力越大。 4 | 5 | 有一个经验法则是:协议越强,优化的空间越少。当程序开发者使用弱协议或弱内存模型时,相应就会有许多优化选择。结果是,这个项目只能由少数专家来维护,而你我可能都不属于专家的范畴。 6 | 7 | 粗略地说,C++11中有三个协议级别。 8 | 9 | ![](../../../images/detail/memory-model/1.png) 10 | 11 | C++11之前,C++不包括多线程或原子。系统只遵循控制流,因此优化的潜力非常有限。该系统的关键是,保证程序开发者所观察到的程序行为,与源代码中指令的顺序一致。当然,这就意味着没有内存模型,只有序列点。序列点是程序中的点,在这些点上的所有指令的效果是可见的,函数执行的开始或结束都是序列点。当使用两个参数调用一个函数时,C++并不保证先计算哪个参数,因此其行为是未指定的,原因很简单——逗号操作符不是序列点。 12 | 13 | C++11中,这些都发生了变化。C++11是C++第一个支持多线程的标准。C++内存模型深受[Java内存模型]( https://en.wikipedia.org/wiki/Java_memory_model)的影响,不过C++内存模型做了很多改进。为了得到定义良好的程序,程序开发者在处理共享变量时必须遵守规则。如果存在数据竞争,则程序的行为是未定义的。如前所述,如果线程共享可变数据,必须注意数据竞争。 14 | 15 | 在使用原子操作的时候,经常会讨论无锁编程。我在本节中谈到了弱规则和强规则,其中原子操作的顺序一致语义被称为**强内存模型**,原子操作的自由语义被称为**弱内存模型**。 16 | 17 | ## 基础 18 | 19 | C++内存模型需要保证以下操作: 20 | 21 | * 原子操作:不受中断地执行。 22 | * 部分排序操作:操作序列的顺序不能重排。 23 | * 可见操作:保证共享变量上的操作对其他线程可见。 24 | 25 | 协议基础是针对原子操作的,其特点是原子的、不可分割的,并且在执行上会创建同步和约束顺序。当然,同步和约束顺序也适用于非原子的操作。一方面,原子类型上的操作总是原子的;另一方面,可以根据需要定制同步和约束顺序。 26 | 27 | ## 挑战 28 | 29 | 内存模型越弱,就能把越多的注意力转放到其他事情上,比如: 30 | 31 | * 优化潜力。 32 | * 控制流数量。 33 | * 了解更多底层的知识。 34 | * 程序行为与我们的预期是否一致。 35 | * 更加微观的优化。 36 | 37 | 我们应该是处理多线程的专家。如果想要处理原子(顺序一致)操作,我们应该打开通向下一个专业级别的大门。想要知道使用获得-释放语义或自由语义时会发生什么,就得向下一个境界迈进了。 38 | 39 | ![](../../../images/detail/memory-model/2.png) 40 | 41 | 我们从无锁编程开始,深入研究C++内存模型。当完成了基础知识的了解后,就要开始真正接触内存模型了。我们的起点是顺序一致语义,接着是获得-释放语义,而自由语义则作为旅程的终章。 42 | 43 | 现在,开启我们的原子操作之旅吧! -------------------------------------------------------------------------------- /content/The-Details/Memory-Model/1.5-chinese.md: -------------------------------------------------------------------------------- 1 | # 栅栏 2 | 3 | C++支持两种栅栏类型:`std::atomic_thread_fence`和`std::atomic_signal_fence`。 4 | 5 | * `std::atomic_thread_fence` : 同步线程间的内存访问。 6 | * `std::atomic_signal_fence` : 线程内信号之间的同步。 7 | 8 | **`std::atomic_thread_fence`** 9 | 10 | `std::atomic_thread_fence`可以阻止特定的操作翻过栅栏。 11 | 12 | `std::atomic_thread_fence`不需要原子变量,通常称为栅栏或内存屏障。那就先来了解一下`std::atomic_thread_fence`。 13 | 14 | ## 栅栏当做内存屏障 15 | 16 | 这个小节的标题什么意思呢?特定的操作不能翻过内存屏障。那什么样的操作属于“特殊操作”呢?现在有两种操作:读写操作或加载/存储操作。` if(resultRead) return result `就是一个加载操作后跟一个存储操作。 17 | 18 | 有四种不同的方式来组合加载和存储操作: 19 | 20 | * 加载-加载:一个加载操作后跟一个加载操作。 21 | * 加载-存储:一个加载操作后跟一个存储操作。 22 | * 存储-加载:一个存储操作后跟一个加载操作。 23 | * 存储-存储:一个存储操作后跟一个存储操作。 24 | 25 | 当然,还有由多个加载和存储(`count++`)组成的更复杂的操作,这些操作都可由以上四个操作组成。 26 | 27 | 那么内存屏障是什么呢?如果在加载-加载、加载-存储、存储-加载或存储-存储等操作之间设置内存屏障,则可以保证不会对特定的操作进行重新排序。如果使用非原子或具有自由语义的原子操作,则存在重新排序的风险。 28 | 29 | ## 三种栅栏类型 30 | 31 | 通常,栅栏有三种:全栅(full fence)、获取栅栏(acquire fence)和释放栅栏(release fence)。提醒一下,获取是一个加载操作, 释放是一个存储操作。如果在加载和存储操作的四种组合之间,放一个内存屏障中会发生什么情况呢? 32 | 33 | * 全栅: 任意两个操作之间使用完整的栅栏`std::atomic_thread_fence()`,可以避免这些操作的重新排序。不过,对于存储-加载操作来说,它们可能会被重新排序。 34 | * 获取栅栏: `std::atomic_thread_fence(std::memory_order_acquire)`避免在获取栅栏之前的读操作,被获取栅栏之后的读或写操作重新排序。 35 | * 释放栅栏: `std::atomic_thread_fence(std::memory_order_release)`避免释放栅栏之后的写操作,在释放栅栏之前通过读或写操作重新排序。 36 | 37 | 为了获得和释放栅栏的定义,以及对无锁编程的影响,我们花费了大量精力对其进行整理。特别难以理解的是,这种栅栏与原子操作获取-释放语义之间的差别。先用图来说明一些上面的定义。 38 | 39 | 哪种操作可以翻过内存屏障?先瞧瞧下面的三张图。如果箭头与红色横杠交叉,意味着栅栏会阻止这种操作。 40 | 41 | **全栅** 42 | 43 | ![](../../../images/detail/memory-model/22.png) 44 | 45 | 当然,可以显式地调用`std::atomic_thread_fence(std::memory_order_seq_cst)`,而不是`std::atomic_thread_fence()`。默认情况下,栅栏使用内存序为顺序一致性。如果对全栏使用顺序一致性,那么`std::atomic_thread_fence`也将遵循全局序。 46 | 47 | **获取栅栏** 48 | 49 | ![](../../../images/detail/memory-model/23.png) 50 | 51 | **释放栅栏** 52 | 53 | ![](../../../images/detail/memory-model/24.png) 54 | 55 | 三种内存屏障可以描述得更简单。 56 | 57 | **所有栅栏一览图** 58 | 59 | ![](../../../images/detail/memory-model/25.png) 60 | 61 | 获取-释放栅栏与原子获取-释放语义有着相似的同步方式和顺序。 62 | 63 | ## 获取-释放栅栏 64 | 65 | 获取-释放栅栏与原子类的获取-释放语义最明显的区别是,栅栏不需要原子操作。还有一个更微妙的区别:获取-释放栅栏比原子操作更重量级。 66 | 67 | ### 原子操作 vs. 栅栏 68 | 69 | 简单起见,现在使用栅栏或带有获取语义的原子操作时引用获取操作,释放操作也是如此。 70 | 71 | 获取-释放操作的主要思想是,在线程间建立同步和排序约束,这些同步和顺序约束也适用于使用自由语义的原子操作或非原子操作。注意,获取-释放操作是成对出现的。此外,对获取-释放语义的原子变量的操作,必须作用在相同的原子变量上。不过,我现在是将这些操作分开来看待的。 72 | 73 | 让我们从获取操作开始对比。 74 | 75 | ### 获取操作 76 | 77 | 在原子变量(内存序为`std::memory_order_acquire`)上进行的加载 (读取)操作是一个获取操作。 78 | 79 | ![](../../../images/detail/memory-model/26.png) 80 | 81 | 将`std::atomic_thread_fence`内存序设置为`std::memory_order_acquire`,这对内存访问重排添加了更严格的约束: 82 | 83 | ![](../../../images/detail/memory-model/27.png) 84 | 85 | 比较中可以总结了两点: 86 | 87 | 1. 具有获取语义的栅栏会建立更强的顺序约束。虽然,原子变量和栅栏的获取操作,要求在获取操作之前不能进行任何读或写操作。但是对获取栅栏有另一种方式,获取栅栏后不能进行读操作。 88 | 2. 自由语义足以读取原子变量`var`。由于`std::atomc_thread_fence(std::memory_order_acquire)`,所以这个操作在获取栅栏之后不能进行读取。 89 | 90 | 对于释放栅栏也可以进行类似的试验。 91 | 92 | ### 释放操作 93 | 94 | 对内存序为`std::memory_order_release`的原子变量,进行存储(写)操作时,这些操作属于释放操作。 95 | 96 | ![](../../../images/detail/memory-model/28.png) 97 | 98 | 还有,释放栅栏。 99 | 100 | ![](../../../images/detail/memory-model/29.png) 101 | 102 | 除了释放操作对原子变量`var`的约束外,释放栅栏有两个属性: 103 | 104 | 1. 存储的操作不能在栅栏前进行。 105 | 2. 变量`var`使用自由语义。 106 | 107 | 现在,就使用栅栏写一段程序。 108 | 109 | ## 使用原子变量或栅栏进行同步 110 | 111 | 之前,我们已经用获取-释放语义,实现了一个典型的消费者-生产者工作流。先使用原子的是原子操作,再切换到栅栏。 112 | 113 | ### 原子操作 114 | 115 | 我们从原子操作开始,大家对它们应该都很熟悉。 116 | 117 | ```c++ 118 | // acquireRelease.cpp 119 | 120 | #include 121 | #include 122 | #include 123 | #include 124 | 125 | using namespace std; 126 | 127 | atomic ptr; 128 | int data; 129 | atomic atoData; 130 | 131 | void producer(){ 132 | string *p = new string("C++11"); 133 | data = 2011; 134 | atoData.store(2014, memory_order_relaxed); 135 | ptr.store(p, memory_order_release); 136 | } 137 | 138 | void consumer(){ 139 | string* p2; 140 | while(!(p2 = ptr.load(memory_order_acquire))); 141 | cout << "*p2: " << *p2 << endl; 142 | cout << "data: " << data << endl; 143 | cout << "atoData: " << atoData.load(memory_order_relaxed) << endl; 144 | } 145 | 146 | int main(){ 147 | 148 | cout << endl; 149 | 150 | thread t1(producer); 151 | thread t2(consumer); 152 | 153 | t1.join(); 154 | t2.join(); 155 | 156 | cout << endl; 157 | 158 | } 159 | ``` 160 | 161 | 这个程序应该很熟悉,这是我们在`std:: memory_order_consumption`小节中使用的示例。下图强调了消费者线程t2看到来自生产者线程t1的所有值。 162 | 163 | ![](../../../images/detail/memory-model/30.png) 164 | 165 | 这段程序定义良好,因为先行关系是可传递的。只需要把三种发生前关系结合起来: 166 | 167 | 1. 第15-17行先行于第18行`ptr.store(p, std:: memory_order_release)`。 168 | 2. 第23行`while(!(p2= ptrl.load(std::memory_order_acquire)))` 先行于第24-26行。 169 | 3. 第18行与第23行同步⇒第18行线程内先行于第23行。 170 | 171 | 现在,事情变得更有趣了,我们要来聊聊栅栏了。有关C++内存模型的文献中,栅栏几乎完全被忽略了。 172 | 173 | ### 栅栏 174 | 175 | 将程序改成到使用栅栏。 176 | 177 | ```c++ 178 | // acquireReleaseFences.cpp 179 | 180 | #include 181 | #include 182 | #include 183 | #include 184 | 185 | using namespace std; 186 | 187 | atomic ptr; 188 | int data; 189 | atomic atoData; 190 | 191 | void producer() { 192 | string* p = new string("C++11"); 193 | data_ = 2011; 194 | atoData.store(2014, memory_order_relaxed); 195 | atomic_thread_fence(memory_order_release); 196 | ptr.store(p, memory_order_release); 197 | } 198 | 199 | void consumer() { 200 | string* p2; 201 | while (!(p2 = ptr.load(memory_order_relaxed))); 202 | atomic_thread_fence(memory_order_acquire); 203 | cout << "*p2: " << *p2 << endl; 204 | cout << "data: " << data_ << endl; 205 | cout << "atoData: " << atoData.load(memory_order_relaxed) << endl; 206 | } 207 | 208 | int main() { 209 | 210 | cout << endl; 211 | 212 | thread t1(producer); 213 | thread t2(consumer); 214 | 215 | t1.join(); 216 | t2.join(); 217 | 218 | delete ptr; 219 | 220 | cout << endl; 221 | 222 | } 223 | ``` 224 | 225 | 第一步是添加栅栏(使用释放和获取语义,第18行和第25行)。接下来,将原子操作从获取或释放语义很容易的改为自由语义(第19和24行)。当然,只能用相应的栅栏替换获取或释放操作。释放栅栏建立了与获取栅栏的同步,因此线程间的也有了先行关系。 226 | 227 | 下图是程序的输出: 228 | 229 | ![](../../../images/detail/memory-model/31.png) 230 | 231 | 为了更直观的呈现给读者,下图是描述了整个关系。 232 | 233 | ![](../../../images/detail/memory-model/32.png) 234 | 235 | 关键问题是:为什么获取栅栏之后的操作,会看到释放栅栏之前的操作呢?因为数据是一个非原子变量`atoData.store`,并且以自由语义使用,这意味着它们可以重新排序;不过,因为`std::atomic_thread_fence(std::memory_order_release)`与`std::atomic_thread_fence(std::memory_order_acquire)`相结合,所以两个操作都不能重新排序。 236 | 237 | 用更简洁的形式进行解释: 238 | 239 | 1. 获取-释放栅栏阻止了原子和非原子操作跨栅栏的重排序。 240 | 2. 消费者线程`t2`正在等待`while (!(p2= ptr.load(std::memory_order_relaxed)))`循环跳出,直到在生产者线程`t1`中设置对指针进行设置`ptr.store(p,std::memory_order_relaxed) `。 241 | 3. 释放栅栏与获取栅栏同步。 242 | 4. 自由操作或非原子操作的所有结果(在释放栅栏之前),在获得栅栏之后都是可见的。 243 | 244 | > **释放栅栏和获取栅栏之间的同步** 245 | > 246 | > 这两个定义来自于[N4659: Working Draft, Standard for Programming Language C++](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf) ,并且标准文档的文字比较难懂:“如果操作X和操作Y对原子对象M的操作存在有原子操作,释放栅栏A同步于获取栅栏B;那么A的操作顺序位于X之前,X对M进行修改,Y位于B之前,并且Y读取X写入的值,或在进行释放操作时,释放序列X中的任何操作所写的值将被读取。” 247 | > 248 | >让我借由acquireReleaseFence.cpp解释一下这段话: 249 | > 250 | >* `atomic_thread_fence(memory_order_release)` (第18行)是一个释放栅栏A。 251 | > * `atomic_thread_fence(memory_order_acquire)` (第25行)是一个获取栅栏B。 252 | >* `ptr`(第10行)是一个原子对象M。 253 | > * `ptr.store(p, memory_order_relaxed)` (第19行) 是一个原子存储操作X。 254 | > * `while (!(p2 = ptr.load(memory_order_relaxed)))` (第24行)是一个原子加载操作Y。 255 | 256 | 可以在acquireRelease.cpp程序中的原子变量上,混合获取和释放操作(使用获取和释放栅栏),而不影响同步关系。 257 | 258 | #### std::atomic_signal_fence 259 | 260 | `std::atomic_signal_fence`在线程和信号句柄间,建立了非原子和自由原子访问的内存同步序。下面的程序展示了`std::atomic_signal_fence`的用法。 261 | 262 | ```c++ 263 | // atomicSignal.cpp 264 | 265 | #include 266 | #include 267 | #include 268 | 269 | std::atomic a{false}; 270 | std::atomic b{false}; 271 | 272 | extern "C" void handler(int){ 273 | if (a.load(std::memory_order_relaxed)){ 274 | std::atomic_signal_fence(std::memory_order_acquire); 275 | assert(b.load(std::memory_order_relaxed)); 276 | } 277 | } 278 | 279 | int main(){ 280 | 281 | std::signal(SIGTERM, handler); 282 | 283 | b.store(true, std::memory_order_relaxed); 284 | std::atomic_signal_fence(std::memory_order_release); 285 | a.store(true, std::memory_order_relaxed); 286 | 287 | } 288 | ``` 289 | 290 | 首先,第19行中为特定的信号SIGTERM设置了处理句柄。SIGTERM是程序的终止请求。` std::atomic_signal_handler`在释放操作`std:: signal_fence(std::memory_order_release)`(第22行)和获取操作`std:: signal_fence(std::memory_order_acquire)`(第12行)之间建立一个获取-释放栅栏。释放操作不能跨越释放栅栏进行重排序(第22行),而获取操作不能跨越获取栅栏进行重排序(第11行)。因此,第13行`assert(b.load(std::memory_order_relax)`的断言永远不会触发,因为`a.store(true, std:: memory_order_relaxed)`(第23行)执行了的话, `b.store(true, std::memory_order_relax)`(第21行)就一定执行过。 291 | 292 | -------------------------------------------------------------------------------- /content/The-Details/Multithreading/2.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 多线程 2 | 3 | C++11添加了多线程接口,为创建多线程程序提供了基础件。多线程的基础件有:线程、共享数据(如互斥锁和锁)的同步原语、线程本地数据、线程(如条件变量)的同步机制和任务。任务(通常称为promise和future)会提供了比线程更高级的抽象。 4 | 5 | ![](../../../images/detail/multithreading/1.png) -------------------------------------------------------------------------------- /content/The-Details/Multithreading/2.1-chinese.md: -------------------------------------------------------------------------------- 1 | #线程 2 | 3 | 要用C++标准库启动一个线程,就必须包含``头文件。 4 | 5 | ## 创建线程 6 | 7 | 线程`std::thread`对象表示一个可执行单元。当工作包是可调用单元时,工作包可以立即启动。线程对象是不可复制构造或复制赋值的,但可移动构造或移动赋值。 8 | 9 | 可调用单元是行为类似于函数。当然,它可以是一个函数,也可以是一个函数对象,或者一个Lambda表达式。通常忽略可调用单元的返回值。 10 | 11 | 介绍完理论知识之后,我们来动手写个小例子。 12 | 13 | ```c++ 14 | // createThread.cpp 15 | 16 | #include 17 | #include 18 | 19 | void helloFunction() { 20 | std::cout << "Hello from a function." << std::endl; 21 | } 22 | 23 | class HelloFUncitonObject { 24 | public: 25 | void operator()()const { 26 | std::cout << "Hello from a function object." << std::endl; 27 | } 28 | }; 29 | 30 | int main() { 31 | 32 | std::cout << std::endl; 33 | 34 | std::thread t1(helloFunction); 35 | HelloFUncitonObject helloFunctionObject; 36 | std::thread t2(helloFunctionObject); 37 | 38 | std::thread t3([] {std::cout << "Hello from a lambda." << std::endl; }); 39 | 40 | t1.join(); 41 | t2.join(); 42 | t3.join(); 43 | 44 | std::cout << std::endl; 45 | 46 | } 47 | ``` 48 | 49 | 三个线程(`t1`、`t2`和`t3`)都会将信息写入控制台。线程`t2`的工作包是一个函数对象(第10 - 15行),线程`t3`的工作包是一个Lambda函数(第26行)。第28 - 30行,主线程在等待子线程完成工作。 50 | 51 | 看一下输出。 52 | 53 | ![](../../../images/detail/multithreading/2.png) 54 | 55 | 三个线程以任意顺序执行,这三个输出操作也可以交错。 56 | 57 | 线程的创建者(例子中是主线程)负责管理线程的生命周期,所以让我们来了解一下线程的生命周期。 58 | 59 | ##线程的生命周期 60 | 61 | 父母需要照顾自己的孩子,这个简单的原则对线程的生命周期非常重要。下面的程序(子线程最后没有汇入),用来显示线程ID。 62 | 63 | ```c++ 64 | #include 65 | #include 66 | 67 | int main() { 68 | 69 | std::thread t([] {std::cout << std::this_thread::get_id() << std::endl; }); 70 | 71 | } 72 | ``` 73 | 74 | 程序出现了错误,不过依旧打印了线程的ID。 75 | 76 | ![](../../../images/detail/multithreading/3.png) 77 | 78 | 那是什么原因引起的异常呢? 79 | 80 | **汇入和分离** 81 | 82 | 线程`t`的生命周期终止于可调用单元执行结束,而创建者有两个选择: 83 | 84 | 1. 等待线程完成: `t.join()` 85 | 2. 与创建线程解除关系:`t.detach() ` 86 | 87 | 当后续代码依赖于线程中调用单元的计算结果时,需要使用`t.join()`。`t.detach()`允许线程与创建线程分离执行,所以分离线程的生命周期与可执行文件的运行周期相关。通常,服务器上长时间运行的后台服务,会使用分离线程。 88 | 89 | 如果`t.join()`和`t.detach()`都没有执行,那么线程`t`是可汇入的。可汇入线程的析构函数会抛出`std::terminate`异常,这也就是threadWithoutJoin.cpp程序产生异常的原因。如果在线程上多次调用`t.join()`或`t.detach()`,则会产生`std::system_error`异常。 90 | 91 | 解决问题的方法很简单:使用`t.join()`。 92 | 93 | ```c++ 94 | #include 95 | #include 96 | 97 | int main() { 98 | 99 | std::thread t([] {std::cout << std::this_thread::get_id() << std::endl; }); 100 | 101 | t.join(); 102 | 103 | } 104 | ``` 105 | 106 | 现在就能得到满意的输出了。 107 | 108 | ![](../../../images/detail/multithreading/4.png) 109 | 110 | 线程ID是`std::thread`唯一的标识符。 111 | 112 | > **分离线程的挑战** 113 | > 114 | > 当然,可以在最后一个程序中使用`t.detach()`代替`t.join()`。这样,线程`t`不能汇入了;因此,它的析构函数没有调用`std::terminate`函数。但现在有另一个问题:未定义行为。主程序可能在线程`t`前结束,所以由于主线程的生存期太短,无法显示ID。详细信息,可以参考变量的生存期。 115 | 116 | >**Anthony Williams提出的scoped_thread** 117 | > 118 | >如果手动处理线程的生命周期可能有些麻烦,可以在包装器中封装`std::thread`。如果线程仍然是可汇入的,这个类应该在其析构函数中自动调用`t.join()`,也可以反过来调用`t.detach()`,但分离处理也有问题。 119 | > 120 | >Anthony Williams提出了这样一个类,并在他的优秀著作[《C++ Concurrency in Action》](https://www.manning.com/books/c-plus-plus-concurrency-in-action)中介绍了它。他将包装器称为`scoped_thread`。`scoped_thread`在构造函数中获取了线程对象,并检查线程对象是否可汇入。如果传递给构造函数的线程对象不可汇入,则不需要`scoped_thread`。如果线程对象可汇入,则析构函数调用`t.join()`。因为,复制构造函数和复制赋值操作符被声明为`delete`,所以`scoped_thread`的实例不能复制或赋值。 121 | > 122 | >```c++ 123 | >// scoped_thread.cpp 124 | > 125 | >#include 126 | >#include 127 | > 128 | >class scoped_thread{ 129 | >std::thread t; 130 | >public: 131 | > explicit scoped_thread(std::thread t_): t(std::move(t_)){ 132 | > if (!t.joinable()) throw std::logic_error("No thread"); 133 | > } 134 | > ~scoped_thread(){ 135 | > t.join(); 136 | > } 137 | > scoped_thread(scoped_thread&)= delete; 138 | > scoped_thread& operator=(scoped_thread const &)= delete; 139 | >}; 140 | >``` 141 | 142 | ## 线程参数 143 | 144 | 和函数一样,线程可以通过复制、移动或引用来获取参数。`std::thread`是一个[可变参数模板]( http://en.cppreference.com/w/cpp/language/parameter_pack),可以传入任意数量的参数。 145 | 146 | 线程通过引用的方式获取数据的情况,必须非常小心参数的生命周期和数据的共享方式。 147 | 148 | ### 复制或引用 149 | 150 | 我们来看一个代码段。 151 | 152 | ```c++ 153 | std::string s{"C++11"} 154 | 155 | std::thread t1([=]{ std::cout << s << std::endl; }); 156 | t1.join(); 157 | 158 | std::thread t2([&]{ std::cout << s << std::endl; }); 159 | t2.detach(); 160 | ``` 161 | 162 | 线程`t1`通过复制的方式获取参数,线程`t2`通过引用的方式获取参数。 163 | 164 | > **线程的“引用”参数** 165 | > 166 | > 实际上,我骗了你。线程`t2`不是通过引用获取其参数,而是Lambda表达式通过引用捕获的参数。如果需要引用将参数传递给线程,则必须将其包装在[引用包装器](http://en.cppreference.com/w/cpp/utility/functional/reference_wrapper)中,使用[std::ref](http://en.cppreference.com/w/cpp/utility/functional/ref)就能完成这项任务。`std::ref`在``头文件中定义。 167 | > 168 | > ```c++ 169 | > 170 | > ... 171 | > void transferMoney(int amount, Account& from, Account& to){ 172 | > ... 173 | > } 174 | > ... 175 | > std::thread thr1(transferMoney, 50, std::ref(account1), std::ref(account2)); 176 | > ``` 177 | > 178 | > 线程`thr1`执行`transferMoney`函数。`transferMoney`的参数是使用引用的方式传递,所以线程`thr1`通过引用获取`account1`和`account2`。 179 | 180 | 这几行代码中隐藏着什么问题呢?线程`t2`通过引用获取其字符串`s`,然后从其创建者的生命周期中分离。字符串`s`与创建者的生存期周期绑定,全局对象`std::cout`与主线程的生存周期绑定。因此,`std::cout`的生存周期可能比线程`t2`的生存周期短。现在,我们已经置身于未定义行为中了。 181 | 182 | 不相信?来看看未定义行为是什么样的。 183 | 184 | ```c++ 185 | // threadArguments.cpp 186 | 187 | #include 188 | #include 189 | #include 190 | 191 | class Sleeper { 192 | public: 193 | Sleeper(int& i_) :i{ i_ } {}; 194 | void operator()(int k) { 195 | for (unsigned int j = 0; j <= 5; ++j) { 196 | std::this_thread::sleep_for(std::chrono::microseconds(100)); 197 | i += k; 198 | } 199 | std::cout << std::this_thread::get_id() << std::endl; 200 | } 201 | private: 202 | int& i; 203 | }; 204 | 205 | 206 | int main() { 207 | 208 | std::cout << std::endl; 209 | 210 | int valSleepr = 1000; 211 | std::thread t(Sleeper(valSleepr), 5); 212 | t.detach(); 213 | std::cout << "valSleeper = " << valSleepr << std::endl; 214 | 215 | std::cout << std::endl; 216 | 217 | } 218 | ``` 219 | 220 | 问题在于:`valSleeper`在第29行时值是多少?`valSleeper`是一个全局变量。线程`t`获得一个函数对象,该函数对象的实参为变量`valSleeper`和数字5(第27行),而线程通过引用获得`valSleeper`(第9行),并与主线程(第28行)分离。接下来,执行函数对象的调用操作符(第10 - 16行),它从0计数到5,在每100毫秒的中休眠,将`k`加到`i`上。最后,屏幕上显示它的id。[Nach Adam Riese](https://de.wikipedia.org/wiki/Liste_gefl%C3%BCgelter_Worte/N#Nach_Adam_Riese) (德国成语:真是精准的计算呀!),期望的结果应该是1000 + 6 * 5 = 1030。 221 | 222 | 然而,发生了什么?结果为什么完全不对? 223 | 224 | ![](../../../images/detail/multithreading/5.png) 225 | 226 | 这个输出有两个奇怪的地方:首先,`valSleeper`是1000;其次,ID没有显示。 227 | 228 | 这段程序至少有两个错误: 229 | 230 | 1. `valSleeper`是线程共享的。这会导致数据竞争,因为线程可能同时读写`valSleeper`。 231 | 2. 主线程的生命周期很可能在子线程执行计算,或将其ID写入`std::cout`之前结束。 232 | 233 | 这两个问题都是构成竞态条件,因为程序的结果取决于操作的交错。构成竞态的条件也是导致数据竞争的原因。 234 | 235 | 解决数据竞争也非常容易:使用锁或原子保护`valSleeper`。为了解决`valSleeper`和`std::cout`的生命周期问题,必须汇入线程而不是分离它。 236 | 237 | 修改后的主函数体。 238 | 239 | ```c++ 240 | int main(){ 241 | 242 | std::cout << std::endl; 243 | 244 | int valSleeper= 1000; 245 | std::thread t(Sleeper(valSleeper),5); 246 | t.join(); 247 | std::cout << "valSleeper = " << valSleeper << std::endl; 248 | 249 | std::cout << std::endl; 250 | 251 | } 252 | ``` 253 | 254 | 现在,我们得到了正确的结果。当然,执行速度会变慢。 255 | 256 | ![](../../../images/detail/multithreading/6.png) 257 | 258 | 为了更完整的了解`std::thread`,接下来了解其成员函数。 259 | 260 | ###成员函数 261 | 262 | 下面是`std::thread`的接口,在一个简洁的表中。更多详情请访问[cppreference.com](http://de.cppreference.com/w/cpp/thread/thread)。 263 | 264 | | 函数名称 | 描述 | 265 | | :----------------------------------------: | :----------------------------------------: | 266 | | `t.join()` | 等待,直到线程t完成 | 267 | | `t.detach()` | 独立于创建者执行创建的线程t | 268 | | `t.joinable()` | 如果线程t可以汇入,则返回true | 269 | | `t.get_id()`和`std::this_thread::get_id()` | 返回线程的ID | 270 | | `std::thread::hardware_concurrency()` | 返回可以并发运行的线程数 | 271 | | `std::this_thread::sleep_until(absTime)` | 将线程t置为睡眠状态,直到absTime时间点为止 | 272 | | `std::this_thread::sleep_for(relTime)` | 将线程t置为睡眠状态,直到休眠了relTime为止 | 273 | | `std::this_thread::yield()` | 允许系统运行另一个线程 | 274 | | `t.swap(t2)`和`std::swap(t1, t2)` | 交换线程对象 | 275 | 276 | 静态函数`std::thread::hardware_concurrency`返回实现支持的并发线程数量,如果运行时无法确定数量,则返回0(这是根据C++标准编写的)。`sleep_until`和`sleep_for`操作需要一个时间点或持续时间作为参数。 277 | 278 | > **访问特定系统的实现** 279 | > 280 | > 线程接口是底层实现的包装器,可以使用`native_handle`来访问(特定于系统的实现)。这个底层实现的句柄可用于线程、互斥对象和条件变量。 281 | 282 | 作为对本小节的总结,下面是在实践中提到的一些方法。 283 | 284 | ```c++ 285 | // threadMethods.cpp 286 | 287 | #include 288 | #include 289 | 290 | using namespace std; 291 | 292 | int main() { 293 | 294 | cout << boolalpha << endl; 295 | 296 | cout << "hardware_concurrency() = " << thread::hardware_concurrency() << endl; 297 | 298 | thread t1([] {cout << "t1 with id = " << this_thread::get_id() << endl; }); 299 | thread t2([] {cout << "t2 with id = " << this_thread::get_id() << endl; }); 300 | 301 | cout << endl; 302 | 303 | cout << "FROM MAIN: id of t1 " << t1.get_id() << endl; 304 | cout << "FROM MAIN: id of t2 " << t2.get_id() << endl; 305 | 306 | cout << endl; 307 | swap(t1, t2); 308 | 309 | cout << "FROM MAIN: id of t1 " << t1.get_id() << endl; 310 | cout << "FROM MAIN: id of t2 " << t2.get_id() << endl; 311 | 312 | cout << endl; 313 | 314 | cout << "FROM MAIN: id of main= " << this_thread::get_id() << endl; 315 | 316 | cout << endl; 317 | 318 | cout << "t1.joinable(): " << t1.joinable() << endl; 319 | 320 | cout << endl; 321 | 322 | t1.join(); 323 | t2.join(); 324 | 325 | cout << endl; 326 | 327 | cout << "t1.joinable(): " << t1.joinable() << endl; 328 | 329 | cout << endl; 330 | 331 | } 332 | ``` 333 | 334 | 与输出相结合来看,应该很容易理解。 335 | 336 | ![](../../../images/detail/multithreading/7.png) 337 | 338 | 结果可能看起来有点奇怪,线程`t1`和`t2`(第14行和第15行)在不同时间点上运行。无法确定每个线程何时运行,只能确定在第38和39行`t1.join()`和`t2.join()`语句之前两个线程是肯定运行了的。 339 | 340 | 线程共享的可变(非const)变量越多,程序的风险就越大。 341 | 342 | -------------------------------------------------------------------------------- /content/The-Details/Multithreading/2.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 线程-本地数据 2 | 3 | 线程-本地数据(也称为线程-本地存储)是为每个线程单独创建的,其行为类似于静态数据。在命名空间范围内,或作为静态类成员的线程局部变量,是在第一次使用之前创建,而在函数中声明的线程局部变量是在第一次使用时创建,并且线程-本地数据只属于线程。 4 | 5 | ```c++ 6 | // threadLocal.cpp 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | std::mutex coutMutex; 14 | 15 | thread_local std::string s("hello from "); 16 | 17 | void addThreadLocal(std::string const& s2) { 18 | 19 | s += s2; 20 | // protect std::cout 21 | std::lock_guard guard(coutMutex); 22 | std::cout << s << std::endl; 23 | std::cout << "&s: " << &s << std::endl; 24 | std::cout << std::endl; 25 | 26 | } 27 | 28 | int main() { 29 | 30 | std::cout << std::endl; 31 | 32 | std::thread t1(addThreadLocal, "t1"); 33 | std::thread t2(addThreadLocal, "t2"); 34 | std::thread t3(addThreadLocal, "t3"); 35 | std::thread t4(addThreadLocal, "t4"); 36 | 37 | t1.join(); 38 | t2.join(); 39 | t3.join(); 40 | t4.join(); 41 | 42 | } 43 | ``` 44 | 45 | 通过在第10行中使用关键字`thread_local`,可以创建线程本地字符串`s`。线程`t1` - `t4`(第27 - 30行)使用`addThreadLocal`函数(第12 - 21行)作为工作包。线程分别获取字符串`t1`到`t4`作为参数,并添加到线程本地字符串`s`中。另外,`addThreadLocal`在第18行会打印`s`的地址。 46 | 47 | ![](../../../images/detail/multithreading/18.png) 48 | 49 | 程序的输出在第17行显示内容,在第18行显示地址。要为字符串`s`创建线程本地字符串:首先,每个输出显示新的线程本地字符串;其次,每个字符串都有不同的地址。 50 | 51 | 我经常在研讨会上讨论:静态变量、`thread_local`变量和局部变量之间的区别是什么?静态变量与主线程的生命周期相同,`thread_local`变量与其所在线程的生存周期相同,而局部变量与创建作用域的生存周期相同。为了说明我的观点,来看一下代码。 52 | 53 | ```c++ 54 | // threadLocalState.cpp 55 | 56 | #include 57 | #include 58 | #include 59 | #include 60 | 61 | std::mutex coutMutex; 62 | 63 | thread_local std::string s("hello from "); 64 | 65 | void first() { 66 | s += "first "; 67 | } 68 | 69 | void second() { 70 | s += "second "; 71 | } 72 | 73 | void third() { 74 | s += "third"; 75 | } 76 | 77 | void addThreadLocal(std::string const& s2) { 78 | 79 | s += s2; 80 | 81 | first(); 82 | second(); 83 | third(); 84 | // protect std::cout 85 | std::lock_guard guard(coutMutex); 86 | std::cout << s << std::endl; 87 | std::cout << "&s: " << &s << std::endl; 88 | std::cout << std::endl; 89 | 90 | } 91 | 92 | int main() { 93 | 94 | std::cout << std::endl; 95 | 96 | std::thread t1(addThreadLocal, "t1: "); 97 | std::thread t2(addThreadLocal, "t2: "); 98 | std::thread t3(addThreadLocal, "t3: "); 99 | std::thread t4(addThreadLocal, "t4: "); 100 | 101 | t1.join(); 102 | t2.join(); 103 | t3.join(); 104 | t4.join(); 105 | 106 | } 107 | ``` 108 | 109 | 代码中,函数`addThreadLocal `(第24行)先调用函数`first` ,然后调用`second`,再调用`third` 。每个函数都使用`thread_local`字符串`s`来添加它的函数名。这种变化的关键之处在于,字符串`s`在函数`first`、`second`和`third`中操作时,处于一种本地数据的状态(第28 - 30行),并且从输出表明字符串是独立存在的。 110 | 111 | ![](../../../images/detail/multithreading/19.png) 112 | 113 | > **单线程到多线程** 114 | > 115 | > 线程本地数据有助于将单线程程序移植成多线程程序。如果全局变量是线程局部的,则可以保证每个线程都得到其数据的副本,从而避免数据竞争。 116 | 117 | 与线程-本地数据相比,条件变量的使用门槛更高。 118 | 119 | -------------------------------------------------------------------------------- /content/The-Details/Multithreading/2.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 条件变量 2 | 3 | 条件变量通过消息对线程进行同步(需要包含``头文件),一个线程作为发送方,另一个线程作为接收方,接收方等待来自发送方的通知。条件变量的典型用例:发送方-接收方或生产者-消费者模式。 4 | 5 | 条件变量`cv`的成员函数 6 | 7 | | 成员函数 | 函数描述 | 8 | | :---------------------------------: | :------------------------------------------------------: | 9 | | `cv.notify_one()` | 通知一个等待中的线程 | 10 | | `cv.notify_all()` | 通知所有等待中的线程 | 11 | | `cv.wait(lock, ...)` | 持有`std::unique_lock`,并等待通知 | 12 | | `cv.wait_for(lock, relTime, ...)` | 持有`std::unique_lock`,并在给定的时间段内等待通知 | 13 | | `cv.wait_until(lock, absTime, ...)` | 持有`std::unique_lock`的同时,并在给定的时间点前等待通知 | 14 | | `cv.native_handle()` | 返回条件变量的底层句柄 | 15 | 16 | `cv.notify_one`和`cv.notify_all`相比较,`cv.notify_all`会通知所有正在等待的线程,`cv.notify_one`只通知一个正在等待的线程,其他条件变量依旧保持在等待状态。介绍条件变量的详细信息之前,来看个示例。 17 | 18 | ```c++ 19 | // conditionVariable.cpp 20 | 21 | #include 22 | #include 23 | #include 24 | #include 25 | 26 | std::mutex mutex_; 27 | std::condition_variable condVar; 28 | 29 | bool dataReady{ false }; 30 | 31 | void doTheWork() { 32 | std::cout << "Processing shared data." << std::endl; 33 | } 34 | 35 | void waitingForWork() { 36 | std::cout << "Worker: Waiting for work." << std::endl; 37 | std::unique_lock lck(mutex_); 38 | condVar.wait(lck, [] {return dataReady; }); 39 | doTheWork(); 40 | std::cout << "Work done." << std::endl; 41 | } 42 | 43 | void setDataReady() { 44 | { 45 | std::lock_guard lck(mutex_); 46 | dataReady = true; 47 | } 48 | std::cout << "Sender: Data is ready." << std::endl; 49 | condVar.notify_one(); 50 | } 51 | 52 | int main() { 53 | 54 | std::cout << std::endl; 55 | 56 | std::thread t1(waitingForWork); 57 | std::thread t2(setDataReady); 58 | 59 | t1.join(); 60 | t2.join(); 61 | 62 | std::cout << std::endl; 63 | 64 | } 65 | ``` 66 | 67 | 该程序有两个子线程:`t1`和`t2`。第38行和第39行中,线程得到工作包`waitingForWork`和`setDataRead`。`setDataReady`使用条件变量`condVar`通知其他线程准备工作已经完成:`condVar.notify_one()`。当持有锁时,线程`t1`等待它的通知:`condVar.wait(lck, []{ return dataReady; }) `。发送方和接收方需要一个锁,对于发送方,`std::lock_guard`就足够了,因为`lock`和`unlock`只调用一次;对于接收方来说,`std::unique_lock`是必需的,因为它需要锁定和解锁互斥锁。 68 | 69 | 程序的输出如下: 70 | 71 | ![](../../../images/detail/multithreading/20.png) 72 | 73 | > **std::condition_variable_any** 74 | > 75 | > `std::condition_variable`只能等待类型为`std::unique_lock`的对象,但是`std::condition_variable_any`可以等待符合[BasicLockable]( http://en.cppreference.com/w/cpp/concept/BasicLockable)原则的锁类型。`std::condition_variable_any`与`std::condition_variable`支持的接口相同。 76 | 77 | ## 谓词 78 | 79 | 在没有谓词的情况下也可以调用`wait`,那么读者朋友应该很想知道,为什么调用`wait`需要谓词。 80 | 81 | 等待使用谓词与否都是可以的,先来看个例子。 82 | 83 | ```c++ 84 | // conditionVariableBlock.cpp 85 | 86 | #include 87 | #include 88 | #include 89 | #include 90 | 91 | std::mutex mutex_; 92 | std::condition_variable condVar; 93 | 94 | void waitingForWork() { 95 | 96 | std::cout << "Worker: Waiting for work." << std::endl; 97 | 98 | std::unique_lock lck(mutex_); 99 | condVar.wait(lck); 100 | // do the work 101 | std::cout << "Work done." << std::endl; 102 | 103 | } 104 | 105 | void setDataReady() { 106 | 107 | std::cout << "Sender: Data is ready." << std::endl; 108 | condVar.notify_one(); 109 | 110 | } 111 | 112 | int main() { 113 | 114 | std::cout << std::endl; 115 | 116 | std::thread t1(setDataReady); 117 | std::thread t2(waitingForWork); 118 | 119 | t1.join(); 120 | t2.join(); 121 | 122 | std::cout << std::endl; 123 | 124 | } 125 | ``` 126 | 127 | 程序的第一次运行正常,但第二次阻塞是因为通知(第25行)发生在线程`t2`(第34行)进入等待状态(第16行)之前。 128 | 129 | ![](../../../images/detail/multithreading/21.png) 130 | 131 | 现在就很清楚了,谓词是无状态条件变量,所以等待过程中总是检查谓词。条件变量有两个已知有害现象:未唤醒和伪唤醒。 132 | 133 | ## 未唤醒和伪唤醒 134 | 135 | **未唤醒** 136 | 137 | 该现象是发送方在接收方到达其等待状态之前发送通知,结果是通知丢失了。C++标准将条件变量描述为同步机制:“条件变量类是同步原语,可用于阻塞一个线程,或同时阻塞多个线程……”所以通知丢失了,接收者就会持续等待…… 138 | 139 | **伪唤醒** 140 | 141 | 还有一种情况,就会没有发通知,但接收方会被唤醒。使用[POSIX Threads](https://en.wikipedia.org/wiki/POSIX_Threads)和 [Windows API](https://en.wikipedia.org/wiki/Windows_API)时,都会出现这样的现象。伪唤醒的真相,很可能是本来就没有处于休眠状态。这意味着,在被唤醒的线程有机会运行之前,另一个线程早就等候多时了。 142 | 143 | ## 等待线程的工作流程 144 | 145 | 等待线程的工作流程相当复杂。 146 | 147 | 下面是来自前面示例conditionVariable.cpp的19和20行。 148 | 149 | ```c++ 150 | std::unique_lock lck(mutex_); 151 | condVar.wait(lck, []{ return dataReady; }); 152 | ``` 153 | 154 | 上面两行与下面四行等价: 155 | 156 | ```c++ 157 | std::unique_lock lck(mutex_); 158 | while ( ![]{ return dataReady; }() { 159 | condVar.wait(lck); 160 | } 161 | ``` 162 | 163 | 首先,必须区分`std::unique_lock lck(mutex_)`的第一次调用与条件变量的通知:`condVar.wait(lck)`。 164 | 165 | * `std::unique_lock lck(mutex_)` : 初始化阶段,线程就将互斥量锁定,并对谓词函数` []{ return dataReady;} `进行检查。 166 | * 谓词返回值: 167 | * true : 线程继续等待。 168 | * false : `condVar.wait()`解锁互斥量,并将线程置为等待(阻塞)状态。 169 | * `condVar.wait(lck)` : 如果`condition_variable condVar`处于等待状态,并获得通知或伪唤醒处于运行状态,则执行以下步骤: 170 | * 线程解除阻塞,重新获得互斥锁。 171 | * 检查谓词函数。 172 | * 当谓词函数返回值为: 173 | * true : 线程继续工作。 174 | * false : `condVar.wait()`解锁互斥量,并将线程置为等待(阻塞)状态。 175 | 176 | 即使共享变量是原子的,也必须在互斥锁保护下进行修改,以便将正确地内容告知等待的线程。 177 | 178 | > **使用互斥锁来保护共享变量** 179 | > 180 | > 即使将`dataReady`设置为原子变量,也必须在互斥锁的保护下进行修改;如果没有,对于等待线程来说`dataReady`的内容就可能是错的,此竞争条件可能导致死锁。让我们再次查看下等待的工作流,并假设`deadReady`是一个原子变量,在不受互斥量`mutex_`保护时进行修改的情况。 181 | > 182 | > ```c++ 183 | >std::unique_lock lck(mutex_); 184 | > while ( ![]{ return dataReady.load(); }() { 185 | > // time window 186 | > condVar.wait(lck); 187 | > } 188 | > ``` 189 | > 190 | > 假设在条件变量`condVar`,在不处于等待状态时发送通知。这样,线程执行到第2行和第4行之间时(参见注释时间窗口)会丢失通知。之后,线程返回到等待状态,可能会永远休眠。 191 | > 192 | > 如果`dataReady`被互斥锁保护,就不会发生这种情况。由于与互斥锁能够同步线程,只有在接收线程处于等待状态的情况下才会发送通知。 193 | 194 | 大多数用例中,可以使用任务,用简单的方式同步线程。“任务-通知”章节中,将条件变量和任务进行了对比。 -------------------------------------------------------------------------------- /content/The-Details/Parallel-Algorithms-of-the-Standard/3.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 标准库的并行算法 2 | 3 | 标准模板库有100多种搜索、计数和范围操作算法。C++17中,重载了69个,并新添加了8个。这些重载版本和新算法,可以使用执行策略来调用。 4 | 5 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/1.png) 6 | 7 | 执行策略可以指定算法串行、并行,还是向量化并行。使用执行策略时,需要包含头文件``。 -------------------------------------------------------------------------------- /content/The-Details/Parallel-Algorithms-of-the-Standard/3.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 执行策略 2 | 3 | C++17标准中定义了三种执行策略: 4 | 5 | * `std::execution::sequenced_policy` 6 | * `std::execution::parallel_policy` 7 | * `std::execution::parallel_unsequenced_policy` 8 | 9 | (译者注:C++20中添加了`unsequenced_policy`策略) 10 | 11 | 相应的策略标定了程序应该串行、并行,还是与向量化并行。 12 | 13 | * `std::execution::seq `: 串行执行 14 | * `std::execution::par `: 多线程并行执行 15 | * `std::execution::par_unseq`: 多个线程上并行,可以循环交叉,也能使用[SIMD]( https://en.wikipedia.org/wiki/SIMD)(单指令多数据) 16 | 17 | `std::execution::par`或`std::execution::par_unseq`允许算法并行或向量化并行。 18 | 19 | 下面的代码片段展示了所有执行策略的使用方式。 20 | 21 | ```c++ 22 | 23 | #include 24 | #include 25 | #include 26 | 27 | int main() { 28 | 29 | std::vector v = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 30 | 31 | // standard sequential sort 32 | std::sort(v.begin(), v.end()); 33 | 34 | // sequential execution 35 | std::sort(std::execution::seq, v.begin(), v.end()); 36 | 37 | // permitting parallel execution 38 | std::sort(std::execution::par, v.begin(), v.end()); 39 | 40 | //permitting parallel and vectorized execution 41 | std::sort(std::execution::par_unseq, v.begin(), v.end()); 42 | 43 | } 44 | ``` 45 | 46 | 示例中,可以使用经典的`std::sort`(第11行)。C++17中,可以明确指定使用方式:串行(第14行)、并行(第17行),还是向量化并行(第20行)。 47 | 48 | `std::is_execution_policy`可以检查模板参数`T`是标准数据类型,还是执行策略类型:`std::is_execution_policy::value`。如果`T`是`std::execution::sequenced_policy`, `std::execution::parallel_policy`, `std::execution::parallel_unsequenced_policy`,或已定义的执行策略类型,则表达式结果为true;否则,为false。 49 | 50 | ## 并行和向量化执行 51 | 52 | 算法是否以并行和向量化的方式运行,取决于许多因素。例如:CPU和编译器是否支持SIMD指令,还取决于编译器实现和代码的优化级别。 53 | 54 | 下面的示例使用循环填充数组。 55 | 56 | ```c++ 57 | 58 | #include 59 | 60 | const int SIZE = 8; 61 | 62 | int vec[] = { 1, 2, 3, 4, 5, 6, 7, 8 }; 63 | int res[] = { 0, 0, 0, 0, 0, 0, 0, 0 }; 64 | 65 | int main() { 66 | 67 | for (int i = 0; i < SIZE; ++i) { 68 | res[i] = vec[i] + 5; 69 | } 70 | 71 | for (int i = 0; i < SIZE; ++i) { 72 | std::cout << res[i] << " "; 73 | } 74 | std::cout << std::endl; 75 | 76 | } 77 | ``` 78 | 79 | 第12行是这个示例中的关键。我们可以在[compiler explorer]( https://godbolt.org)看一下clang 3.6生成的相应汇编指令。 80 | 81 | **无优化** 82 | 83 | 汇编指令中,每个加法都是串行进行的。 84 | 85 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/2.png) 86 | 87 | **使用最高优化级别** 88 | 89 | 通过使用最高的优化级别`-O3`,寄存器(如:xmm0)可以容纳128位,或者说是4个整型数字。这样,加法就可以同时在四个元素进行了。 90 | 91 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/3.png) 92 | 93 | 无执行策略算法的重载,与具有串行执行策略`std::execution::seq`算法的重载在**异常**处理方面有所不同。 94 | 95 | ##异常 96 | 97 | 如果执行策略的算法发生异常,将调用[`std::terminate`](https://en.cppreference.com/w/cpp/error/terminate)。`std::terminate`调用[`std::terminate_handler`](https://en.cppreference.com/w/cpp/error/terminate_handler),之后使用[`std::abort`]( https://en.cppreference.com/w/cpp/utility/program/abort),让异常程序终止。执行策略的算法与调用`std::execution::seq`执行策略的算法之间没有区别。无执行策略的算法会传播异常,因此可以对异常进行处理。exceptionExecutionPolicy.cpp可以佐证我的观点。 98 | 99 | ```c++ 100 | // exceptionExecutionPolicy.cpp 101 | 102 | #include 103 | #include 104 | #include 105 | #include 106 | #include 107 | #include 108 | 109 | int main() { 110 | 111 | std::cout << std::endl; 112 | 113 | std::vector myVec{ 1,2,3,4,5 }; 114 | 115 | try { 116 | std::for_each(myVec.begin(), myVec.end(), 117 | [](int) {throw std::runtime_error("Without execution policy"); } 118 | ); 119 | } 120 | catch (const std::runtime_error & e) { 121 | std::cout << e.what() << std::endl; 122 | } 123 | 124 | try { 125 | std::for_each(std::execution::seq, myVec.begin(), myVec.end(), 126 | [](int) {throw std::runtime_error("With execution policy"); } 127 | ); 128 | } 129 | catch (const std::runtime_error & e) { 130 | std::cout << e.what() << std::endl; 131 | } 132 | catch (...) { 133 | std::cout << "Catch-all exceptions" << std::endl; 134 | } 135 | 136 | } 137 | ``` 138 | 139 | 第21行可以捕获异常`std::runtime_error`,但不能捕获第30行中的异常,甚至在第33行中的捕获全部异常也无法捕获相应的异常。 140 | 141 | 使用新的MSVC编译器,并开启`std:c++latest`选项,程序会给出期望的输出。 142 | 143 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/4.png) 144 | 145 | 只有第一个异常顺利捕获。 146 | 147 | ##数据竞争和死锁的风险 148 | 149 | 并行算法无法避免数据竞争和死锁。 150 | 151 | 下面的并行代码中,就存在数据竞争。 152 | 153 | ```c++ 154 | 155 | #include 156 | #include 157 | 158 | int main() { 159 | 160 | std::vector v = { 1, 2, 3 }; 161 | int sum = 0; 162 | std::for_each(std::execution::par, v.begin(), v.end(), [&sum](int i) { 163 | sum += i + i; 164 | }); 165 | 166 | } 167 | ``` 168 | 169 | 代码段中,`sum`有数据竞争。`sum`上累加了`i + i`的和,并且是并发修改的,所以必须保护`sum`。 170 | 171 | ```c++ 172 | 173 | #include 174 | #include 175 | #include 176 | 177 | std::mutex m; 178 | 179 | int main() { 180 | 181 | std::vector v = { 1, 2, 3 }; 182 | 183 | int sum = 0; 184 | std::for_each(std::execution::par, v.begin(), v.end(), [&sum](int i) { 185 | std::lock_guard lock(m); 186 | sum += i + i; 187 | }); 188 | 189 | } 190 | ``` 191 | 192 | 将执行策略更改为`std::execution::par_unseq`时,会出现条件竞争,并导致死锁。 193 | 194 | ```c++ 195 | 196 | #include 197 | #include 198 | #include 199 | 200 | std::mutex m; 201 | 202 | int main() { 203 | 204 | std::vector v = { 1, 2, 3 }; 205 | 206 | int sum = 0; 207 | std::for_each(std::execution::par_unseq, v.begin(), v.end(), [&sum](int i) { 208 | std::lock_guard lock(m); 209 | sum += i + i; 210 | }); 211 | 212 | } 213 | ``` 214 | 215 | 同一个线程上,Lambda函数可能连续两次调用`m.lock`,这会产生未定义行为,大多数情况下会导致死锁。这里,可以使用原子来避免死锁。 216 | 217 | ```c++ 218 | #include 219 | #include 220 | #include 221 | #include 222 | 223 | std::mutex m; 224 | 225 | int main() { 226 | 227 | std::vector v = { 1, 2, 3 }; 228 | 229 | std::atomic sum = 0; 230 | std::for_each(std::execution::par_unseq, v.begin(), v.end(), [&sum](int i) { 231 | std::lock_guard lock(m); 232 | sum += i + i; 233 | }); 234 | 235 | } 236 | ``` 237 | 238 | 因为`sum`是一个原子计数器,所以将语义放宽也没关系:`sum.fetch_add(i * i, std::memory_order_relaxed)` . 239 | 240 | 执行策略可以作为参数传入69个STL重载算法中,以及C++17添加的8个新算法中。 241 | 242 | -------------------------------------------------------------------------------- /content/The-Details/Parallel-Algorithms-of-the-Standard/3.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 算法 2 | 3 | 下面是69个算法的并行版本。 4 | 5 | | `std::adjacent_difference` | `std::adjacent_find` | `std::all_of` | `std::any_of` | 6 | | :-----------------------------: | :-----------------------: | :-------------------------: | :----------------------------: | 7 | | `std::copy` | `std::copy_if` | `std::copy_n` | `std::count` | 8 | | `std::count_if` | `std::equal` | `std::fill` | `std::fill_n` | 9 | | `std::find` | `std::find_end` | `std::find_first_of` | `std::find_if` | 10 | | `std::find_if_not` | `std::generate` | `std::generate_n` | `std::includes` | 11 | | `std::inner_product` | `std::inplace_merge` | `std::is_heap` | `std::is_heap_until` | 12 | | `std::is_partitioned` | `std::is_sorted` | `std::is_sorted_until` | `std::lexicographical_compare` | 13 | | `std::max_element` | `std::merge` | `std::min_element` | `std::minmax_element` | 14 | | `std::mismatch` | `std::move` | `std::none_of` | `std::nth_element` | 15 | | `std::partial_sort` | `std::partial_sort_copy` | `std::partition` | `std::partition_copy` | 16 | | `std::remove` | `std::remove_copy` | `std::remove_copy_if` | `std::remove_if` | 17 | | `std::replace` | `std::replace_copy` | `std::replace_copy_if` | `std::replace_if` | 18 | | `std::reverse` | `std::reverse_copy` | `std::rotate` | `std::rotate_copy` | 19 | | `std::search` | `std::search_n` | `std::set_difference` | `std::set_intersection` | 20 | | `std::set_symmetric_difference` | `std::set_union` | `std::sort` | `std::stable_partition` | 21 | | `std::stable_sort` | `std::swap_ranges` | `std::transform` | `std::uninitialized_copy` | 22 | | `std::uninitialized_copy_n` | `std::uninitialized_fill` | `std::uninitialized_fill_n` | `std::unique` | 23 | | `std::unique_copy` | | | | 24 | 25 | 除了以上这些算法,还有8种新算法。 26 | 27 | -------------------------------------------------------------------------------- /content/The-Details/Parallel-Algorithms-of-the-Standard/3.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 新算法 2 | 3 | 新算法包含在`std`命名空间中,`std::for_each`和`std::for_each_n`在``头文件中声明,其余六种算法在``头文件中声明。 4 | 5 | 下面是新算法的概述。 6 | 7 | | 算法 | 描述 | 8 | | :-----------------------------: | :----------------------------------------------------------: | 9 | | `std::for_each` | 将一元函数对象应用于引用范围。 | 10 | | `std::for_each_n` | 将一元函数对象应用于引用范围的前n个元素。 | 11 | | `std::exclusive_scan` | 将二元函数对象从左向右应用与引用范围。“排除性”(exclusive)表示第i个输入元素不包含在第i个和内。二元函数对象的第一个参数是之前计算的结果,运算可能以任意顺序进行,并存储中间结果。若二元函数对象不满足结合律,则函数行为不确定。行为与[`std::partial_sum`](http://en.cppreference.com/w/cpp/algorithm/partial_sum)类似。 | 12 | | `std::inclusive_scan` | 将二元函数对象从左向右应用与引用范围。“包含性”(inclusive)表示第i个输入元素包含于第i个和中。二元函数对象的第一个参数是之前计算的结果,运算可能以任意顺序进行,并存储中间结果。若二元函数对象不满足结合律,则函数行为不确定。行为与[`std::partial_sum`](http://en.cppreference.com/w/cpp/algorithm/partial_sum)类似 | 13 | | `std::transform_exclusive_scan` | 首先,将一元函数对象应用于引用范围,然后使用`std::exclusive_scan`。若二元函数对象不满足结合律,则函数行为不确定。 | 14 | | `std::transform_inclusive_scan` | 首先,将一元函数对象应用于引用范围,然后使用`std::inclusive_scan`。若二元函数对象不满足结合律,则函数行为不确定。 | 15 | | `std::reduce` | 将二元函数对象从左向右应用与引用范围。若二元函数对象不满足交换律或结合律,则函数行为不确定。行为与[`std::accumulate`](http://en.cppreference.com/w/cpp/algorithm/accumulate)类似。 | 16 | | `std::transform_reduce` | 首先,将一元函数对象应用于引用范围,然后使用`std::reduce`。若二元函数对象不满足交换律或结合律,则函数行为不确定。 | 17 | 18 | 表中的函数描述不大容易理解,若对`std::accumulate`和`std::partial_sum`比较了解,那对前缀求和算法应该是非常熟悉。归约算法可以并行使用累加的方式,扫描算法可以并行的使用`partial_sum`。这就是`std::reduce`(归约算法)需要满足交换律和结合律的原因。 19 | 20 | 首先,给出一个算法示例,然后介绍这些函数的功能。示例中,忽略了新的`std::for_each`算法。与返回一元函数的C++98实现不同,C++17中什么也不返回。`std::accumulate`从左到右处理元素,而`std::reduce`可以以任意的顺序处理元素。让我们从使用`std::accumulate`和`std::reduce`的小代码段开始,二元函数对象为Lambda函数` [](int a, int b){ return a * b; }`。 21 | 22 | ```c++ 23 | std::vector v{1, 2, 3, 4}; 24 | std::accumulate(v.begin(), v.end(), 1, [](int a, int b){ return a * b; }); 25 | std::reduce(std::execution::par, v.begin(), v.end(), 1 , 26 | [](int a, int b){ return a * b; }); 27 | ``` 28 | 29 | 下面两张图显示了`std::accumulate`和`std::reduce`的不同策略。 30 | 31 | `std::accumulate`从左开始,依次使用二进制操作符。 32 | 33 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/5.png) 34 | 35 | 与`std::accumulate`不同,`std::reduce`以一种不确定的方式使用二元操作符。 36 | 37 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/6.png) 38 | 39 | 40 | 41 | 结合律允许`std::reduce`算法计算任意邻接元素对。因为元素顺序可交换,所以中间结果可以按任意顺序计算。 42 | 43 | > **当前可用的算法实现** 44 | > 45 | > 展示代码之前,必须做个说明。据我所知,本书更新的时候(2018年9月),并没有完全符合标准的并行STL实现。MSVC 17.8也只是增加了对大约30种算法的支持。 46 | > 47 | > MSVC 17.8中的并行算法 48 | > 49 | > | `std::adjacent_difference` | `std::adjacent_find` | `std::all_of` | 50 | >| :------------------------: | :-----------------------------: | :-----------------------------: | 51 | > | `std::any_of` | `std::count` | `std::count_if` | 52 | >| `std::equal` | `std::exclusive_scan` | `std::find` | 53 | > | `std::find_end` | `std::find_first_of` | `std::find_if` | 54 | > | `std::for_each` | `std::for_each_n` | `std::inclusive_scan` | 55 | > | `std::mismatch` | `std::none_of` | `std::reduce` | 56 | > | `std::remove` | `std::remove_if` | `std::search` | 57 | > | `std::search_n` | `std::sort` | `std::stable_sort` | 58 | > | `std::transform` | `std::transform_exclusive_scan` | `std::transform_inclusive_scan` | 59 | > | `std::transform_reduce` | | | 60 | > 61 | > 这里使用HPX实现功能,并生成输出,[HPX (High-Performance ParalleX)](http://stellar.cct.lsu.edu/projects/hpx)是一种可用于任何规模的并行和分布式应用程序的通用C++运行时系统框架。HPX已经在其的一个名称空间中实现了所有并行STL。 62 | > 63 | > 为了完整性,这里是并行STL的部分实现连接: 64 | > 65 | > * [Intel](https://software.intel.com/en-us/get-started-with-pstl) 66 | >* [Thibaut Lutz](https://github.com/t-lutz/ParallelSTL) 67 | > * [Nvidia(thrust)]( https://thrust.github.io/doc/group__execution__policies.html) 68 | >* [Codeplay](https://github.com/KhronosGroup/SyclParallelSTL) 69 | 70 | 新算法示例代码 71 | 72 | ```c++ 73 | // newAlgorithm.cpp 74 | 75 | #include 76 | #include 77 | #include 78 | #include 79 | #include 80 | #include 81 | 82 | 83 | int main() { 84 | 85 | std::cout << std::endl; 86 | 87 | // for_each_n 88 | 89 | std::vector intVec{ 1,2,3,4,5,6,7,8,9,10 }; 90 | std::for_each_n(std::execution::par, 91 | intVec.begin(), 5, [](int& arg) {arg *= arg; }); 92 | 93 | std::cout << "for_each_n: "; 94 | for (auto v : intVec)std::cout << v << " "; 95 | std::cout << "\n\n"; 96 | 97 | // exclusive_scan and inclusive_scan 98 | std::vector resVec{ 1,2,3,4,5,6,7,8,9 }; 99 | std::exclusive_scan(std::execution::par, 100 | resVec.begin(), resVec.end(), resVec.begin(), 1, 101 | [](int fir, int sec) {return fir * sec; }); 102 | 103 | std::cout << "exclusive_scan: "; 104 | for (auto v : resVec)std::cout << v << " "; 105 | std::cout << std::endl; 106 | 107 | std::vector resVec2{ 1,2,3,4,5,6,7,8,9 }; 108 | 109 | std::inclusive_scan(std::execution::par, 110 | resVec2.begin(), resVec2.end(), resVec2.begin(), 111 | [](int fir, int sec) {return fir * sec; }); 112 | 113 | std::cout << "inclusive_scan: "; 114 | for (auto v : resVec2)std::cout << v << " "; 115 | std::cout << "\n\n"; 116 | 117 | // transform_exclusive_scan and transform_inclusive_scan 118 | std::vector resVec3{ 1,2,3,4,5,6,7,8,9 }; 119 | std::vector resVec4(resVec3.size()); 120 | std::transform_exclusive_scan(std::execution::par, 121 | resVec3.begin(), resVec3.end(), 122 | resVec4.begin(), 0, 123 | [](int fir, int sec) {return fir + sec; }, 124 | [](int arg) {return arg *= arg; }); 125 | 126 | std::cout << "transform_exclusive_scan: "; 127 | for (auto v : resVec4)std::cout << v << " "; 128 | std::cout << std::endl; 129 | 130 | std::vector strVec{ "Only", "for","testing", "purpose" }; 131 | std::vector resVec5(strVec.size()); 132 | 133 | std::transform_inclusive_scan(std::execution::par, 134 | strVec.begin(), strVec.end(), 135 | resVec5.begin(), 0, 136 | [](auto fir, auto sec) {return fir + sec; }, 137 | [](auto s) {return s.length(); }); 138 | 139 | std::cout << "transform_inclusive_scan: "; 140 | for (auto v : resVec5) std::cout << v << " "; 141 | std::cout << "\n\n"; 142 | 143 | // reduce and transform_reduce 144 | std::vector strVec2{ "Only", "for", "testing", "purpose" }; 145 | 146 | std::string res = std::reduce(std::execution::par, 147 | strVec2.begin() + 1, strVec2.end(), strVec2[0], 148 | [](auto fir, auto sec) {return fir + ":" + sec; }); 149 | 150 | std::cout << "reduce: " << res << std::endl; 151 | 152 | std::size_t res7 = std::transform_reduce(std::execution::par, 153 | strVec2.begin(), strVec2.end(), 154 | [](std::string s) {return s.length(); }, 155 | 0, [](std::size_t a, std::size_t b) {return a + b; }); 156 | 157 | 158 | std::cout << "transform_reduce: " << res7 << std::endl; 159 | 160 | std::cout << std::endl; 161 | 162 | } 163 | ``` 164 | 165 | 程序在第17行使用了`std::vector`,在第58行使用了`std::vectorstd::string`。 166 | 167 | 第18行中的`std::for_each_n`将向量的前n个元素映射为2次幂。`std::exclusive_scan`(第27行)和`std::inclusive_scan`(第37行)非常相似,两者都对元素应用二元操作。区别在于`std::exclusive_scan`排除了每个迭代中的最后一个元素。 168 | 169 | 第48行中的`std::transform_exclusive_scan`比较难理解。第一步中,使用Lambda函数`[](int arg){return arg *= arg;}`,对`resVec3.begin()`到`resVec3.end()`范围内的每个元素,进行2次幂操作。第二步,对保存中间结果的向量(`resVec4`)使用二元运算`[](int fir, int sec){return fir + sec;}`。这样,使用0作为元素求和的初始值,结果放在`resVec4.begin()`中。 170 | 171 | 第61行中的`std::transform_inclusive_scan`类似,而这里操作的是元素的长度。 172 | 173 | 这里的`std::reduce`应该比较容易理解,程序中在输入向量的每两个元素之间放置“:”字符,因为结果字符串不应该以“:”字符开头,所以从第二个元素`(strVec2.begin() + 1)`开始,并使用`strVec2[0]`作为初始值。 174 | 175 | > **transform_reduce与map_reduce** 176 | > 177 | > 关于第80行的`std::transform_reduce`,我还想多补充两句。首先,C++算法的转换算法,在其他语言中通常称为映射(map)。因此,也可以称`std::transform_reduce`为` std::map_reduce`。`std::transform_reduce`的后端实现,使用的是C++中著名的并行[MapReduce](https://en.wikipedia.org/wiki/MapReduce)算法。相应地,`std::transform_reduce`在某个范围内使用一元函数(`([](std::string s){ return s.length();})`),并将结果归约为一个输出值:`[](std::size_t a, std::size_t b){return a+b;}`。 178 | 179 | 下面是程序的输出。 180 | 181 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/7.png) 182 | 183 | ## 更多的重载 184 | 185 | 归约和扫描算法的C++实现有很多重载版本。最简版本中,可以在没有二元函数对象和初始元素的情况下使用。如果不使用二元函数对象,则默认将加法作为二元操作符。如果没有指定初始元素,则初始元素取决于使用的算法: 186 | 187 | * `std::inclusive_scan`和`std::transform_inclusive_scan`算法 : 选用第一个元素。 188 | * `std::reduce` 和`std::transform_reduce`算法 : 相应类型的构造值`std::iterator_traits::value_type{}`。 189 | 190 | 接下来,从函数的角度再来看看这些新算法。 191 | 192 | ## 功能性继承 193 | 时间宝贵,长话短说:所有的C++新算法在纯函数语言Haskell中都有对应。 194 | 195 | * `std::for_each_n`对应map。 196 | * `std::exclusive_scan`和`std::inclusive_scan` 分别对应scanl和scanl1。 197 | * `std::transform_exclusive_scan`和`std::transform_inclusive_scan` 分别对应map与scan1和scan2的组合。 198 | * `std::reduce`对应foldl或foldl1。 199 | * `std::transform_reduce` 对应于foldl或foldl1与map的组合。 200 | 201 | 展示Haskell的实际效果之前,先了解下功能上的差异。 202 | 203 | * map将一个函数应用于列表。 204 | * foldl和foldl1将一个二元操作符应用于列表,并将该列表的值归约成一个。与foldl1不同,foldl需要一个初始值。 205 | * scanl和scanl1与foldl和foldl1类似,但可以获取计算时的中间结果列表。 206 | * foldl , foldl1 , scanl和scanl1从左向右处理元素。 207 | 208 | 让我们看一下这些Haskell函数,下面是Haskell解释器的命令行界面。 209 | 210 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/8.png) 211 | 212 | (1)和(2)定义了一个整数列表和一个字符串列表。(3)中将Lambda函数`(\a -> a * a)`应用到整数列表中。(4)和(5)比较复杂,表达式(4)以1作为乘法的中间元素,乘以`(*)`所有整数对。表达式(5)做相应的加法运算。理解(6)、(7)和(9)是比较具有挑战性的,必须从右到左读。`scanl1(+).map(\a->length)`(7)是一个函数组合,点`(.)`左右是两个函数。第一个函数将每个元素映射为自身长度,第二个函数将长度列表累加。(9)与(7)相似,不同之处在于`foldl`生成一个值,并需要一个初始值。到这,表达式(8)就好理解了,它连续地用“:”字符将两个字符串连接起来。 -------------------------------------------------------------------------------- /content/The-Details/Parallel-Algorithms-of-the-Standard/3.4-chinese.md: -------------------------------------------------------------------------------- 1 | # 性能概况 2 | 3 | 使用并行STL的首要原因,肯定是性能。 4 | 5 | 下面的代码就能反映不同执行策略的性能差异。 6 | 7 | ```c++ 8 | // parallelSTLPerformance.cpp 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | constexpr long long size = 500'000'000; 20 | 21 | const double pi = std::acos(-1); 22 | 23 | template 24 | void getExecutionTime(const std::string& title, Func func) { 25 | 26 | const auto sta = std::chrono::steady_clock::now(); 27 | func(); 28 | const std::chrono::duration dur = std::chrono::steady_clock::now() - sta; 29 | 30 | std::cout << title << ": " << dur.count() << " sec." << std::endl; 31 | 32 | } 33 | 34 | int main() { 35 | 36 | std::cout << std::endl; 37 | 38 | std::vector randValues; 39 | randValues.reserve(size); 40 | 41 | std::mt19937 engine; 42 | std::uniform_real_distribution<> uniformDist(0, pi / 2); 43 | for (long long i = 0; i < size; ++i) randValues.push_back(uniformDist(engine)); 44 | 45 | std::vector workVec(randValues); 46 | 47 | getExecutionTime("std::execution::seq", [workVec]()mutable { 48 | std::transform(std::execution::seq, workVec.begin(), workVec.end(), 49 | workVec.begin(), 50 | [](double arg) {return std::tan(arg); } 51 | ); 52 | }); 53 | 54 | getExecutionTime("std::execution::par", [workVec]()mutable { 55 | std::transform(std::execution::par, workVec.begin(), workVec.end(), 56 | workVec.begin(), 57 | [](double arg) {return std::tan(arg); } 58 | ); 59 | }); 60 | 61 | getExecutionTime("std::execution::par_unseq", [workVec]()mutable { 62 | std::transform(std::execution::par_unseq, workVec.begin(), workVec.end(), 63 | workVec.begin(), 64 | [](double arg) {return std::tan(arg); } 65 | ); 66 | }); 67 | 68 | } 69 | ``` 70 | 71 | parallelSTLPerformance.cpp统计了串行(第39行)、并行(第46行)和向量化并行(第53行)执行策略的耗时。首先,`randValues`由区间在[0,pi/2)的5亿个数字填充。函数模板`getExecutionTime`(第16 - 24行)获取标题和Lambda函数,在第20行执行Lambda函数,并显示执行耗时(第22行)。程序使用了三个Lambda函数(第39、46和53行),它们被声明为`mutable`。因为Lambda函数修改它的参数`workVec`,而Lambda函数默认是不能对其进行修改的。如果Lambda函数想要修改,那么就必须声明为`mutable`。 72 | 73 | 我的windows笔记本电脑有8个逻辑核心,但并行执行速度要比串行的快10倍以上。 74 | 75 | ![](../../../images/detail/Parallel-Algorithms-of-the-Standard/9.png) 76 | 77 | 并行执行和并行向量化执行的性能大致相同。Visual C++团队的博客对此进行了解释:[使用C++17并行算法更好的性能]( https://blogs.msdn.microsoft.com/vcblog/2018/09/11/using-c17-parallel-algorithms-for-better-performance)。Visual C++团队使用相同的方式实现了并行计算和并行策略,所以目前就不要期望`par_unseq`有更好性能(但未来就不好说了)。 -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.0-chinese.md: -------------------------------------------------------------------------------- 1 | # 新特性:C++20/23 2 | 3 | 这章并不像其他章节那样准确。原因有两个:首先,并不是所有的特性都符合C++20/23标准;其次,如果某个特性符合C++20/23标准,那么该特性的接口很可能会改变。我将定期更新这本书,会将C++标准的最新动态和新的建议在这一章进行更新。 4 | 5 | 本章的目的很简单:让大家了解一下C++中,将会出现的并发特性。 6 | 7 | ![](../../../images/detail/The-Future-CPP-20-23/1.png) -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.1-chinese.md: -------------------------------------------------------------------------------- 1 | # 关于执行 2 | 3 | Executor是C++中执行的基本构造块,在执行中扮演如同容器分配器的角色。异步、标准模板库的并行算法、future的协同、任务块的运行、[网络TS(技术规范,technical specification)](https://en.cppreference.com/w/cpp/experimental)的提交、调度或延迟调用等功能都会使用到异步执行。此外,因为没有标准化的执行方式,所以“执行”是编程时的基本关注点。 4 | 5 | 下面是提案[P0761](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0761r2.pdf)的示例。 6 | 7 | parallel_for的实现 8 | 9 | ```c++ 10 | void parallel_for(int facility, int n, function f) { 11 | if(facility == OPENMP) { 12 | #pragma omp parallel for 13 | for(int i = 0; i < n; ++i) { 14 | f(i); 15 | } 16 | } 17 | else if(facility == GPU) { 18 | parallel_for_gpu_kernel<<>>(f); 19 | } 20 | else if(facility == THREAD_POOL) { 21 | global_thread_pool_variable.submit(n, f); 22 | } 23 | } 24 | ``` 25 | 26 | 这个parallel_for有一些问题: 27 | 28 | * parallel_for这样看起来简单的函数,维护起来其实非常复杂。如果支持新的算法或新的并行范例,会变得越来越复杂。(译者:这里指的是分支中不同平台的实现,如果有新算法或新平台,则函数体会变得越来越臃肿。) 29 | * 函数的每个分支的同步属性也不同。OpenMP可能会阻塞运行,直到所有的派生线程完成,GPU通常异步运行的,线程池可能阻塞或不阻塞。不完全的同步可能会导致数据竞争或死锁。 30 | * parallel_for的限制太多。例如,没有办法使用自定义的线程池替换全局线程池:`global_thread_pool_variable.submit(n, f); ` 31 | 32 | ## 路漫漫其修远兮 33 | 34 | 2018年10月,已经提交了很多关于executor的提案了,许多设计非常开放,真期望它们能成为C++23的一部分,或有可能用C++20对单向执行进行标准化。本章主要是基于对executor的[P0761号提案](](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0761r2.pdf))的设计建议,和在[P0443](http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p0443r7.html)和[P1244](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1244r0.html)提案中的描述进行的。P0443(统一的executor)中提出了单向执行,它可能是C++20的一部分,P1244(统一的executor的从属执行)提出了从属执行,它可能是C++23的一部分。本章还提到了相对较新的[P1055](http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1055r0.pdf)提案,“适当executor提案”。 35 | 36 | ## Executor是什么? 37 | 38 | 什么是executor?executor由一组关于在何处、何时以及如何运行可调用单元的规则组成。 39 | 40 | * 何处: 可调用项可以在内部或外部处理器上运行,并且结果是从内部或外部处理器中进行读取。 41 | * 何时: 可调用项可以立即运行,也可以延迟运行。 42 | * 如何: 可调用项的可以在CPU或GPU上运行,甚至可以以向量化的方式执行。 43 | 44 | 更正式地说,每个executor都具有与所执行函数相关联的属性。 45 | 46 | **Executor属性** 47 | 48 | 可以通过两种方式,将这些属性与executor关联起来:`execution::require`或`execution::prefer ` 49 | 50 | 1. 方向性:执行函数可以是“触发即忘”(`execution::oneway`)、返回一个future(`execution::twoway`)或返回一个continuation(`execution::then`)。 51 | 2. 基数性:执行函数可以创建一个(`execution::single`)或多个执行代理(`execution::bulk`)。 52 | 3. 阻塞性:函数可阻塞也可不阻塞,有三个互斥的阻塞属性:`execution::blocking.never`,`execution::blocking.possibly`和`execution::blocking.always`。 53 | 4. 持续性:任务可能是由客户端上的线程执行(`execution::continuation`),也可能不执行(`execution::not_continuation`)。 54 | 5. 可溯性:指定跟踪未完成的工作(`exection::outstanding_work`),或不跟踪(`execution::outstanding_work.untracked`)。 55 | 6. 批量进度保证:指定在批量属性,`execution::bulk_sequenced_execution`、`execution::bulk_parallel_execution`和`execution::bulk_unsequenced_execution`,这些属性是互斥的,通过使用这些属性创建的执行代理,可以保证任务的进度。 56 | 7. 执行线程映射:将每个执行代理映射到一个新线程(`execution::new_thread_execution_mapping`),或者不映射(`execution::thread_execution_mapping`)。 57 | 8. 分配器:将分配器(`execution::allocator`)与executor关联起来。 58 | 59 | 也可以自己来定义属性。 60 | 61 | > Executor是基础构建块 62 | > 63 | > 因为executor是执行的构建块,C++的并发性和并行性特性在很大程度上依赖于它们。这也适用于扩展future,网络的[N4734](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4734.pdf)扩展,甚至是适用于STL的并行算法,以及C++20/23中的新并发特性,如门闩和栅栏、协程、事务性内存和任务块。 64 | 65 | ## 举个例子 66 | 67 | **使用Executor** 68 | 69 | 下面的代码片段,展示了executor的用法: 70 | 71 | **std::async** 72 | 73 | ```c++ 74 | // get an executor through some means 75 | my_executor_type my_executor = ... 76 | 77 | // launch an async using my executor 78 | auto future = std::async(my_executor, [] { 79 | std::cout << "Hello world, from a new execution agent!" << std::endl; 80 | }); 81 | ``` 82 | 83 | **STL算法std::for_each** 84 | 85 | ```c++ 86 | // get an executor through some means 87 | my_executor_type my_executor = ... 88 | 89 | // execute a parallel for_each "on" my executor 90 | std::for_each(std::execution::par.on(my_executor), 91 | data.begin(), data.end(), func); 92 | ``` 93 | 94 | **网络技术规范:允许客户端连接默认系统Executor** 95 | 96 | ```c++ 97 | // obtain an acceptor (a listening socket) through some means 98 | tcp::acceptor my_acceptor = ... 99 | 100 | // perform an asynchronous operation to accept a new connection 101 | acceptor.async_accept( 102 | [](std::error_code ec, tcp::socket new_connection) 103 | { 104 | ... 105 | } 106 | ); 107 | ``` 108 | 109 | **网络技术规范:允许客户端连接带有线程池的Executor** 110 | 111 | ```c++ 112 | // obtain an acceptor (a listening socket) through some means 113 | tcp::acceptor my_acceptor = ... 114 | 115 | // obtain an executor for a specific thread pool 116 | auto my_thread_pool_executor = ... 117 | 118 | // perform an asynchronous operation to accept a new connection 119 | acceptor.async_accept( 120 | std::experimental::net::bind_executor(my_thread_pool_executor, 121 | [](std::error_code ec, tcp::socket new_connection) 122 | { 123 | ... 124 | } 125 | ) 126 | ); 127 | ``` 128 | 129 | 网络技术规范[N4734](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4734.pdf)的`std::experimental::net::bind_executor`函数允许使用特定的executor。本例中,程序在线程池中执行Lambda函数。 130 | 131 | 要使用executor ,必须进行获取。 132 | 133 | **获取Executor** 134 | 135 | 获取Executor的方法有很多。 136 | 137 | **源于自执行上下文static_thread_pool** 138 | 139 | ```c++ 140 | // create a thread pool with 4 threads 141 | static_thread_pool pool(4); 142 | 143 | // get an executor from the thread pool 144 | auto exec = pool.executor(); 145 | 146 | // use the executor on some long-running task 147 | auto task1 = long_running_task(exec); 148 | ``` 149 | 150 | **源自执行策略std:: Execution::par** 151 | 152 | ```c++ 153 | // get par's associated executor 154 | auto par_exec = std::execution::par.executor(); 155 | 156 | // use the executor on some long-running task 157 | auto task2 = long_running_task(par_exec); 158 | ``` 159 | 160 | **源于系统的Executor ** 161 | 162 | 通常使用线程执行的默认程序。如果有变量没有指定,那就可以使用它。 163 | 164 | **源于Executor适配器** 165 | 166 | ```c++ 167 | // get an executor from a thread pool 168 | auto exec = pool.executor(); 169 | 170 | // wrap the thread pool's executor in a logging_executor 171 | logging_executor logging_exec(exec); 172 | 173 | // use the logging executor in a parallel sort 174 | std::sort(std::execution::par.on(logging_exec), my_data.begin(), my_data.end()); 175 | ``` 176 | 177 | logging_executo是循环executor的包装器。 178 | 179 | ## Executor的目标 180 | 181 | 提案[P1055]( http://open-std.org/JTC1/SC22/WG21/docs/papers/2018/p1055r0.pdf)中,executor的目的是什么呢? 182 | 183 | 1. 批量化:权衡可调用单元的转换成本和大小。 184 | 2. 异构化:允许可调用单元在异构上下文中运行,并能返回结果。 185 | 3. 有序化:可指定调用顺序,可选的顺序有:后进先出[LIFO](https://en.wikipedia.org/wiki/Stack_(abstract_data_type))、先进先出[FIFO](https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)) 、优先级或耗时顺序,甚至是串行执行。 186 | 4. 可控化:可调用的对象必须是特定计算资源的目标,可以延迟,也可以取消。 187 | 5. 持续化:需要可调用信号来控制异步,这些信号必须指示结果是否可用、是否发生了错误、何时完成或调用方是否希望取消,并且显式启动或停止可调用项也应该是可以的。 188 | 6. 层级化:层次结构允许在不增加用例复杂性的情况下添加功能。 189 | 7. 可用化:易实现和易使用,应该是主要目标。 190 | 8. 组合化:允许用户扩展executor的功能。 191 | 9. 最小化:executor中不应该存在任何库外添加的内容。 192 | 193 | ## 术语 194 | 195 | 提案[P0761](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0761r2.pdf)为可执行单元定义了一些执行的新术语: 196 | 197 | * 执行资源:能够执行可调用的硬件和/或软件,执行单元可以是SIMD,也可以是管理大量线程集合的运行时。CPU或GPU的执行资源是异构的,所以它们有不同的限制。 198 | * 执行上下文:是一个程序对象,表示特定的执行资源集合和这些资源中的执行代理。典型的例子是线程池、分布式运行时或异构运行时。 199 | * 执行代理:特定执行单元的上下文,该上下文映射到执行资源上的单个可调用单元。典型的例子是CPU线程或GPU执行单元。 200 | * 执行器:与特定上下文关联的执行对象。提供一个或多个执行函数,用于创建可调用函数对象的执行代理。 201 | 202 | ## 执行函数 203 | 204 | 执行程序可提供一个或多个执行函数,用于创建可调用对象的执行代理。执行程序至少支持以下六个功能中的一个。 205 | 206 | | 名称 | 基数性 | 方向性 | 207 | | :-----------------: | :----: | :----: | 208 | | execute | 单个 | oneway | 209 | | twoway_execute | 单个 | twoway | 210 | | then_execute | 单个 | then | 211 | | bulk_execute | 批量 | oneway | 212 | | bulk_twoway_execute | 批量 | twoway | 213 | | bulk_then_execute | 批量 | then | 214 | 215 | 每个执行函数都有两个属性:基数性和方向性。 216 | 217 | * 基数性 218 | * 单个: 创建一个执行代理 219 | * 批量 : 创建一组执行代理 220 | * 方向性 221 | * oneway : 创建执行代理,但不返回结果 222 | * twoway : 创建一个执行代理,并返回一个可用于等待执行完成的future 223 | * then : 创建一个执行代理,并返回一个可用于等待执行完成的future。给定的future准备好后,执行代理开始执行。 224 | 225 | 让我更简单的解释一下执行功能,他们都有一个可执行单元。 226 | 227 | **基数性:单个** 228 | 229 | 单个基数性很简单,单向执行函数是以“触发即忘”的方式执行,返回void。它非常类似于“触发即忘”的future,但它不会自动阻止future的销毁。twoway执行函数返回future,可以使用它来获取结果。类似于`std::promise`,它将返回关联`std::future`的句柄。这种情况下,执行代理仅在提供的future准备好时才运行。 230 | 231 | **基数性:批量** 232 | 233 | 批量基数性的情况比较复杂。这些函数创建一组执行代理,每个执行代理调用给定的可调用单元`f`,它们返回一个结果代理。`f`的第一个参数是`shape`参数,它是一个整型,代表代理类型的索引。进一步的参数是结果代理,如果是twoway执行器,那么就和所有代理共享`shape`代理。用于创建共享代理的参数,其生存期与代理的生存期绑定在一起。因为它们能够通过执行可调用单元产生相应的价值,所以称为代理。客户端负责通过这个结果代理,消除结果的歧义。 234 | 235 | 使用bulk_then_execute函数时,可调用单元`f`将其之前的future作为附加参数。因为没有代理是所有者,所以可调用单元`f`可通过引用获取结果、共享参数和前次结果。 236 | 237 | **execution::require** 238 | 239 | 如何确保执行程序支持特定的执行功能? 240 | 241 | 在特殊情况下,你需要对其有所了解。 242 | 243 | ```c++ 244 | void concrete_context(const my_oneway_single_executor& ex) 245 | { 246 | auto task = ...; 247 | ex.execute(task); 248 | } 249 | ``` 250 | 251 | 通常情况下,可以使用函数`execution::require`来申请。 252 | 253 | ```c++ 254 | template 255 | void generic_context(const Executor& ex) 256 | { 257 | auto task = ...; 258 | // ensure .toway_execute() is available with execution::require() 259 | execution::require(ex, execution::single, execution::twoway).toway_execute(task); 260 | } 261 | ``` 262 | 263 | ## 实现原型 264 | 265 | 基于提案[P0443R5]( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0443r5.html),executor提案有了具体的实现原型。这个实现原型,可以帮助我们更深入地了解了批量基数。 266 | 267 | ```c++ 268 | // executor.cpp 269 | 270 | #include 271 | #include 272 | #include 273 | #include 274 | 275 | namespace execution = std::experimental::execution; 276 | using std::experimental::static_thread_pool; 277 | using std::experimental::executors_v1::future; 278 | 279 | int main() { 280 | 281 | static_thread_pool pool{ 4 }; 282 | auto ex = pool.executor(); 283 | 284 | // One way, single 285 | ex.execute([] {std::cout << "We made it!" << std::endl; }); 286 | 287 | std::cout << std::endl; 288 | 289 | // Two way, single 290 | future f1 = ex.twoway_execute([] {return 42; }); 291 | f1.wait(); 292 | std::cout << "The result is: " << f1.get() << std::endl; 293 | 294 | std::cout << std::endl; 295 | 296 | // One way, bulk. 297 | ex.bulk_execute([](int n, int& sha) { 298 | std::cout << "part " << n << ": " << "shared: " << sha << "\n"; 299 | }, 8, 300 | [] {return 0; } 301 | ); 302 | 303 | std::cout << std::endl; 304 | 305 | // Two way, bulk, void result 306 | future f2 = ex.bulk_twoway_execute( 307 | [](int n, std::atomic& m) { 308 | std::cout << "async part " << n; 309 | std::cout << " atom: " << m++ << std::endl; 310 | }, 8, 311 | [] {}, 312 | [] { 313 | std::atomic atom(0); 314 | return std::ref(atom); 315 | } 316 | ); 317 | f2.wait(); 318 | std::cout << "bulk result available" << std::endl; 319 | 320 | std::cout << std::endl; 321 | 322 | // Two way, bulk, non-void result. 323 | future f3 = ex.bulk_twoway_execute( 324 | [](int n, double&, int&) { 325 | std::cout << "async part " << n << " "; 326 | std::cout << std::this_thread::get_id() << std::endl; 327 | }, 8, 328 | [] { 329 | std::cout << "Result factory: " 330 | << std::this_thread::get_id() << std::endl; 331 | return 123.456; }, 332 | [] { 333 | std::cout << "Shared Parameter: " 334 | << std::this_thread::get_id() << std::endl; 335 | return 0; } 336 | ); 337 | f3.wait(); 338 | std::cout << "bulk result is " << f3.get() << std::endl; 339 | 340 | } 341 | ``` 342 | 343 | 该程序使用具有四个线程的线程池进行执行(第14行和第15行)。第18行和第23行使用单基数的执行函数,并创建两个单基数的代理。第二个是twoway执行函数,因此返回一个结果。 344 | 345 | 第30、39和56行中的执行函数具有批量基数性。每个函数创建8个代理(第32、43和60行)。第一种情况中,可调用单元会显示索引`n`和共享值`sha`,`sha`是由共享代理在第33行创建的。下一个执行函数`bulk_twoway_execute`更有趣。虽然它的结果代理返回void,但共享状态是原子变量`atom`。每个代理将其值增加1(第42行)。通过结果代理,最后一个执行函数(第56到69行)返回123.456。有趣的是,在可调用的执行、结果和共享代理的执行中涉及到多少线程呢?程序的输出显示结果和共享代理运行在同一个线程中,而其他代理运行在不同的线程中。 346 | 347 | ![](../../../images/detail/The-Future-CPP-20-23/2.png) -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.2-chinese.md: -------------------------------------------------------------------------------- 1 | # 可协作中断的线程 2 | 3 | `std::jthread`代表协作线程,除了C++11添加的`std::thread`外,`std::jthread`还可以自动汇入启动的线程,并发出中断信号。它的特性在提案[P0660R8](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0660r8.pdf)中进行了详细描述:可中断的协程。 4 | 5 | ## 自动汇入 6 | 7 | 下面`std::thread`的行为并不乐观。如果`std::thread`仍是可汇入的,则在其析构函数中调用`std::terminate`。如果调用了`thre .join()`或`thre .detach()`,则线程`thr`是可汇入的。 8 | 9 | ```c++ 10 | // threadJoinable.cpp 11 | 12 | #include 13 | #include 14 | 15 | int main() { 16 | 17 | std::cout << std::endl; 18 | std::cout << std::boolalpha; 19 | 20 | std::thread thr{ [] {std::cout << "Joinable std::thread" << std::endl; } }; 21 | 22 | std::cout << "thr.joinable(): " << thr.joinable() << std::endl; 23 | 24 | std::cout << std::endl; 25 | 26 | } 27 | ``` 28 | 29 | 程序执行的时候,会崩溃掉。 30 | 31 | ![](../../../images/detail/The-Future-CPP-20-23/3.png) 32 | 33 | 运行了两次,`std::thread`都会非法终止。第二次运行时,线程`thr`有显示了消息:“Joinable std::thread”。 34 | 35 | 下一个示例中,我将头文件``替换为`“jthread.hpp”`。并使用C++20标准中的`std::jthread`。 36 | 37 | ```c++ 38 | // jthreadJoinable.cpp 39 | 40 | #include 41 | #include "jthread.hpp" 42 | 43 | int main() { 44 | 45 | std::cout << std::endl; 46 | std::cout << std::boolalpha; 47 | 48 | std::jthread thr{ [] {std::cout << "Joinable std::thread" << std::endl; } }; 49 | 50 | std::cout << "thr.joinable(): " << thr.joinable() << std::endl; 51 | 52 | std::cout << std::endl; 53 | 54 | } 55 | ``` 56 | 57 | 现在,如果线程`thr`会在调用析构时还是可汇入的,则会自动汇入。 58 | 59 | ![](../../../images/detail/The-Future-CPP-20-23/4.png) 60 | 61 | ## 中断std::jthread 62 | 63 | 为了理解其中的思想,我举一个简单的例子。 64 | 65 | ```c++ 66 | // interruptJthread.cpp 67 | 68 | #include "jthread.hpp" 69 | #include 70 | #include 71 | 72 | using namespace ::std::literals; 73 | 74 | int main() { 75 | 76 | std::cout << std::endl; 77 | 78 | std::jthread nonInterruptable([] { 79 | int counter{ 0 }; 80 | while (counter < 10) { 81 | std::this_thread::sleep_for(0.2s); 82 | std::cerr << "nonInterruptable: " << counter << std::endl; 83 | ++counter; 84 | } 85 | }); 86 | 87 | std::jthread interruptable([](std::stop_token stoken) { 88 | int counter{ 0 }; 89 | while (counter < 10) { 90 | std::this_thread::sleep_for(0.2s); 91 | if (stoken.stop_requested()) return; 92 | std::cerr << "interruptable: " << counter << std::endl; 93 | ++counter; 94 | } 95 | }); 96 | 97 | std::this_thread::sleep_for(1s); 98 | 99 | std::cerr << std::endl; 100 | std::cerr << "Main thread interrupts both jthreads" << std::endl; 101 | nonInterruptable.request_stop(); 102 | interruptable.request_stop(); 103 | 104 | std::cout << std::endl; 105 | 106 | } 107 | ``` 108 | 109 | 主程序中启动了两个线程`nonInterruptable`和`interruptable`(第13行和第22行)。与线程`nonInterruptable`不同,线程`interruptable `会获取一个`std::stop_token`,并在26行使用它来检查线程是否被中断:`stoken.stop_requested()`。在中断的情况下返回Lambda函数,然后线程结束。`interruptable.request_stop() `(第37行)触发线程的结束。而`nonInterruptable.request_stop()`并没有什么效果。 110 | 111 | ![](../../../images/detail/The-Future-CPP-20-23/5.png) 112 | 113 | 下面来了解停止令牌、汇入线程和条件变量的更多细节。 114 | 115 | ## 停止令牌 116 | 117 | `jthread`的附加功能基于`std::stop_token`、`std::stop_callback`和`std::stop_source`。 118 | 119 | **std::stop_token , std::stop_source 和std::stop_callback** 120 | 121 | `std::stop_token`、`std::stop_callback`或`std::stop_source`使其能够异步请求执行停止,或查询执行是否收到了停止信号。可以将`std::stop_token`传递给操作,然后使用它来主动轮询停止请求的令牌,或者通过`std::stop_callback`注册回调。停止请求由`std::stop_source`发送,这个信号影响所有相关的`std::stop_token`。`std::stop_source`、`std::stop_token`和`std::stop_callback`共享停止状态的所有权,其中`request_stop()`、`stop_requested()`和`stop_possible()`是原子操作。 122 | 123 | `std::stop_source`和`std::stop_token`组件为停止处理提供了以下属性。 124 | 125 | `std::stop_source src`的成员函数 126 | 127 | | 成员函数 | 功能描述 | 128 | | :------------------: | :----------------------------------------------------------: | 129 | | src.get_token() | 如果!stop_possible(),则构造一个不共享stop的stop_token对象状态;否则,构造一个stop_token对象,并共享使用*this的停止状态 | 130 | | src.stop_possible() | 如果停止源可以用于请求停止,则为true | 131 | | src.stop_requested() | 如果其中一个所有者调用了stop_possible()和request_stop(),则为true。 | 132 | | src.request_stop() | 如果!stop_possible()或stop_requested(),则调用没有效果;否则,提出一个停止请求,以便同步调用stop_requested() == true和所有已注册的回调。 | 133 | 134 | ` std::stop_token stoken`的成员函数 135 | 136 | | 成员函数 | 功能描述 | 137 | | :---------------------: | :----------------------------------------------------------: | 138 | | stoken.stop_possible() | 如果后续调用stop_required()将永远不会返回true | 139 | | stoken.stop_requested() | 如果在相关的std::stop_source上调用了request_stop(),则为true,否则为false | 140 | 141 | 如果`std::stop_token`临时禁用了,那么可以用默认构造的令牌替换它。默认构造的令牌无效。下面的代码片段展示了,如何禁用和启用线程接受信号的功能。 142 | 143 | 临时禁用一个`std::stop_token` 144 | 145 | ```c++ 146 | std::jthread jthr([](std::stop_token stoken){ 147 | ... 148 | std::stop_token interruptDisabled; 149 | std::swap(stoken, interruptDisabled); 150 | ... 151 | std::swap(stoken, interruptDisabled); 152 | ... 153 | } 154 | ``` 155 | 156 | `std::stop_token interruptDisabled`是无效的。这意味着,从第4行到第5行停止令牌被禁用,第6行才启用。 157 | 158 | 下面的示例展示了回调的用法。 159 | 160 | ```c++ 161 | // invokeCallback.cpp 162 | 163 | #include "jthread.hpp" 164 | #include 165 | #include 166 | #include 167 | 168 | using namespace ::std::literals; 169 | 170 | auto func = [](std::stop_token stoken) { 171 | int counter{ 0 }; 172 | auto thread_id = std::this_thread::get_id(); 173 | std::stop_callback callBack(stoken, [&counter, thread_id] { 174 | std::cout << "Thread id: " << thread_id 175 | << "; counter : " << counter << std::endl; 176 | }); 177 | while (counter < 10) { 178 | std::this_thread::sleep_for(0.2s); 179 | ++counter; 180 | } 181 | }; 182 | 183 | int main() { 184 | 185 | std::cout << std::endl; 186 | 187 | std::vector vecThreads(10); 188 | for (auto& thr : vecThreads)thr = std::jthread(func); 189 | 190 | std::this_thread::sleep_for(1s); 191 | 192 | for (auto& thr : vecThreads)thr.request_stop(); 193 | 194 | std::cout << std::endl; 195 | 196 | } 197 | ``` 198 | 199 | 这10个线程中的每个都调用Lambda函数func(第10 - 21行)。第13 - 16行中的回调显示线程id和计数器。由于主线程的睡眠时间为1秒,子线程的睡眠时间为1秒,所以调用回调时计数器为4。`request_stop()`会在每个线程上触发回调。 200 | 201 | ![](../../../images/detail/The-Future-CPP-20-23/6.png) 202 | 203 | **汇入线程** 204 | 205 | `std::jhread`是一个`std::thread`变种,它具有发出中断信号,并自动汇入的附加功能。为了支持这个功能,它需要一个`std::stop_token`。 206 | 207 | `std::jthread jthr`停止令牌的成员函数 208 | 209 | | 成员函数 | 功能描述 | 210 | | :--------------------: | :----------------------: | 211 | | jthr.get_stop_source() | 返回stop_token | 212 | | jthr.request_stop() | 与src.request_stop()相同 | 213 | 214 | **condition_variable_any成员函数wait的新重载** 215 | 216 | `std::condition_variable_any`的三个`wait`变体`wait_for`和`wait_until`将有新的重载,新的重载会使用`std::stop_token`。 217 | 218 | ```c++ 219 | template 220 | bool wait_until(Lock& lock, 221 | Predicate pred, 222 | stop_token stoken); 223 | 224 | template 225 | bool wait_until(Lock& lock, 226 | const chrono::time_point& abs_time, 227 | Predicate pred, 228 | stop_token stoken); 229 | 230 | template 231 | bool wait_for(Lock& lock, 232 | const chrono::duration& rel_time, 233 | Predicate pred, 234 | stop_token stoken); 235 | ``` 236 | 237 | 这个新的重载需要一个谓词函数。该版本在传入的`std::stop_token stoken`发出中断信号时,得到通知。这三个重载相当于下面的表达式: 238 | 239 | ```c++ 240 | // wait_until in lines 1 - 4 241 | while(!pred() && !stoken.stop_requested()) { 242 | wait(lock, [&pred, &stoken] { 243 | return pred() || stoken.stop_requested(); 244 | }); 245 | } 246 | return pred(); 247 | 248 | // wait_until in lines 6 - 10 249 | while(!pred() && !stoken.stop_requested() && Clock::now() < abs_time) { 250 | cv.wait_until(lock, 251 | abs_time, 252 | [&pred, &stoken] { 253 | return pred() || stoken.stop_requested(); 254 | }); 255 | } 256 | return pred(); 257 | 258 | // wait_for in lines 12 - 16 259 | return wait_until(lock, chrono::steady_clock::now() + rel_time, std::move(pred), std\ 260 | ::move(stoken)); 261 | ``` 262 | 263 | 调用`wait`之后,可以对停止请求进行检查。 264 | 265 | ```c++ 266 | cv.wait_until(lock, predicate, stoken); 267 | if (stoken.stop_requested()){ 268 | // interrupt occurred 269 | } 270 | ``` 271 | 272 | -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.3-chinese.md: -------------------------------------------------------------------------------- 1 | # 原子智能指针 2 | 3 | `std::shared_ptr`由控制块和相关资源组成。`std::shared_ptr`能够保证控制块是线程安全的,但是对相关资源的访问就不是了。这意味着,修改引用计数器是一个原子操作,可以确保资源删除一次。 4 | 5 | > **线程安全的重要性** 6 | > 7 | > 这里只说明`std::shared_ptr`具有定义良好的多线程语义是有多么重要。乍一看,使用`std::shared_ptr`并不是多线程程序的明智选择。根据定义,它是共享和可变的,是数据竞争和未定义行为的理想对象。另一方面,现代C++中有一条准则:不要接触内存。这意味着在多线程程序中,要尽可能使用智能指针。 8 | 9 | 关于原子智能指针的[N4162](http://wg21.link/n4162)提议,直接解决了当前智能指针实现的缺陷。这些缺陷可以归结为以下三点:一致性、正确性和高效性。下面将概述这三点,详系内容可参见提案N4162。 10 | 11 | * 一致性:`std::shared_ptr`对非原子数据类型,只能进行原子操作。 12 | * 正确性:因为正确的使用方式是基于严格的规则,所以使用全局性的原子操作非常容易出错。很容易忘记使用原子操作——例如,使用`ptr = localPtr`代替`std::atomic_store(&ptr, localPtr)`。由于数据竞争,结果是未定义的。如果使用原子智能指针,系统将不允许数据竞争的出现。 13 | * 高效性:与`atomic_*`函数相比,原子智能指针有很大的优势。原子版本是为特殊用例设计的,可以在内部使用`std::atomic_flag`作为一种低开销的自旋锁。如果将指针函数的非原子版设计为线程安全的,并用于单线程场景,那就太大材小用了,并且还会受到性能上的惩罚。 14 | 15 | 对我来说,正确性是最重要的。为什么?答案就在提案中。这个建议提供了一个线程安全的单链表,它支持插入、删除和搜索元素,并且这个单链表以无锁的方式实现。 16 | 17 | ## 线程安全的单链表 18 | 19 | ![](../../../images/detail/The-Future-CPP-20-23/7.png) 20 | 21 | 需要使用C++11编译器编译的地方都用红色标记。这个链表,使用原子智能指针实现要容易得多,也不容易出错。C++20的类型系统不允许在原子智能指针上使用非原子操作。 22 | 23 | [N4162](http://wg21.link/n4162)提议将`std::atomic_shared_ptr`和`std::atomic_weak_ptr`作为原子智能指针。将它们合并到主流的ISO C++标准中,就变成了`std::atomic`: `std::atomicstd::shared_ptr`和`std::atomicstd::weak_ptr`偏特化模板。 24 | 25 | 因此,`std::shared_ptr`的原子操作在C++20中是废弃的。 -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.5-chinese.md: -------------------------------------------------------------------------------- 1 | # 门闩和栅栏 2 | 3 | 门闩和栅栏是比较简单的线程同步机制,其能使一些线程阻塞,直到计数器变为零时解除阻塞。首先,不要把栅栏和内存栅栏混为一谈。C++ 20/23中,我们假设有三种门闩和栅栏:`std::latch`、`std::barrier`和`std::flex_barrier`。 4 | 5 | 首先,要回答两个问题: 6 | 7 | 1. 这三种同步线程的机制有什么不同?`std::latch`只能使用一次,但是`std::barrier`和`std::flex_barrier`可以使用多次。此外,`std::flex_barrier`允许计数器变为0时执行一个函数。 8 | 2. 哪些支持的门闩和栅栏的用例,在C++11和C++14中无法通过future、线程或条件变量与锁结合来实现呢?门闩和栅栏并不涉及新的用例,但它们使用起来要容易得多。通常是在内部使用无锁机制,所以它们还具有更高的性能。 9 | 10 | ##std::latch 11 | 12 | `std::latch`门闩是一个倒计时器,该值可以在构造函数中设置。门闩可以通过使用`latch.count_down_and_wait`来减小计数,并阻塞线程,直到计数器变为0。另外,`latch.count_down(n)`可以将计数器减少n,而不进行阻塞。如果没有给出参数,n默认为1。门闩也有`latch.is_ready`可以用来检查计数器是否为零,以及`latch.wait`会阻塞线程,直到计数器变为零。`std::latch`的计数器不能增加或重置,因此不能复用。 13 | 14 | 下面是来自[N4204提案]( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4204.html)的一个简短代码段。 15 | 16 | ```c++ 17 | void DoWork(threadpool *pool){ 18 | latch completion_latch(NTASKS); 19 | for (int i = 0; i < NTASKS; ++i){ 20 | pool->add_task([&]{ 21 | // perform work 22 | ... 23 | completion_latch.count_down(); 24 | }); 25 | } 26 | // Block until work is done 27 | completion_latch.wait(); 28 | } 29 | ``` 30 | 31 | `std::latch completion_latch`在其构造函数中将计数器设置为NTASKS (第2行),线程池执行NTASKS(第4 - 8行)个任务。每个任务结束时(第7行),计数器递减。第11行是运行DoWork函数的线程,以及工作流的栅栏。这样,线程就会阻塞,直到所有任务都完成。 32 | 33 | `std::barrier`与`std::latch`非常相似。 34 | 35 | ##std::barrier 36 | 37 | `std::latch`和`std::barrier`之间的区别是,`std::barrier`计数器可以重置,所以可以多次地使用。计数器变为零之后,立即进入完成阶段。与`std::flex_barrier`有关,`std::barrier`有一个空的完成阶段。`std::barrier`有两个有趣的成员函数:`std::arrive_and_wait`和`std::arrive_and_drop`。当`std::arrive_and_wait`在同步点阻塞时,`std::arrive_and_drop`会从相关线程集中,删除自己的线程。未指定此函数是否阻塞,直到完成阶段结束。这里没有对函数块进行指定,是否到完成阶段才算结束。 38 | 39 | > **N4204提案** 40 | > 41 | > 该建议使用`vector`,并将动态分配的线程推给vector:`workers.push_back(new thread([&]{ ... }))`。这会产生内存泄漏。应该将线程放到`std::unique_ptr`中,或者直接在vector中进行创建: `workers.emplace_back([&]{ ... })`,这个适用于`std::barrier`和`std::flex_barrier`。本例中使用`std::flex_barrier`的名称有点迷,例如:`std::flex_barrier`被称为`notifying_barrier`。所以我把名字改成`flex_barrier`,会更容易理解一些。此外,代表线程数量的`n_threads`没有初始化,我把它初始化为NTASKS。 42 | 43 | 深入研究`std::flex_barrier`和完成阶段之前,这里给出一个简短的示例,演示`std::barrier`的用法。 44 | 45 | **std::barrier** 46 | 47 | ```c++ 48 | void DoWork(){ 49 | Tasks& tasks; 50 | int n_threads{NTASKS}; 51 | vector workes; 52 | 53 | barrier task_barrier(n_threads); 54 | 55 | for (int i = 0; i < n_threads; ++i){ 56 | workers.push_back(new thread([&]{ 57 | bool active = ture; 58 | while(active){ 59 | Task task = tasks.get(); 60 | // perform task 61 | ... 62 | task_barrier.arrive_and_wait(); 63 | } 64 | }); 65 | } 66 | // Read each stage of the task until all stages are complete. 67 | while(!finished()){ 68 | GetNextStage(tasks); 69 | } 70 | } 71 | ``` 72 | 73 | 第6行中的`barrier`用于协调多个执行线程,线程的数量是`n_threads`(第3行),每个线程通过`tasks.get()`获取(第12行中)任务,执行该任务并阻塞(第15行),直到所有线程完成其任务为止。之后,在第12行接受一个新任务,`active`在第11行返回true。 74 | 75 | 与`std::barrier`不同,`std::flex_barrier`多一个构造函数。 76 | 77 | ## std::flex_barrier 78 | 79 | 此构造函数接受在完成阶段调用可调用单元。可调用单元必须返回一个数字,使用这个数字设置计数器的值,返回-1意味着计数器在下一次迭代中保持相同的计数器值,而小于-1的数字是不允许的。 80 | 81 | 完成阶段会执行以下步骤: 82 | 83 | 1. 阻塞全部线程 84 | 2. 任意个线程解除阻塞,并执行可调用单元。 85 | 3. 如果完成阶段已经完成,那么所有线程都将解除阻塞。 86 | 87 | 下面的段代码展示了`std::flex_barrier`的用法 88 | 89 | ```c++ 90 | void DoWork(){ 91 | Tasks& tasks; 92 | int initial_threads; 93 | int n_threads{NTASKS}; 94 | atomic current_threads(initial_threads); 95 | vector workers; 96 | 97 | // Create a flex_barrier, and set a lambda that will be 98 | // invoked every time the barrier counts down. If one or more 99 | // active threads have completed, reduce the number of threads. 100 | std::function rf = [&]{return current_threads;}; 101 | flex_barrier task_barrier(n_threads, rf); 102 | 103 | for (int i = 0; i < n_threads; ++i){ 104 | workers.push_back(new thread([&]{ 105 | bool active = true; 106 | while(active) { 107 | Task task = tasks.get(); 108 | // perform task 109 | ... 110 | if (finished(task)){ 111 | current_threads--; 112 | active = false; 113 | } 114 | task_barrier.arrive_and_wait(); 115 | } 116 | })); 117 | } 118 | 119 | // Read each stage of the task until all stages are cpmplete. 120 | while(!finished()){ 121 | GetNextStage(tasks); 122 | } 123 | } 124 | ``` 125 | 126 | 这个例子采用了与`std::barrier`类似的策略,不同的是这次`std::flex_barrier`计数器是在运行时进行调整,所以`std::flex_barrier task_barrier`在第11行获得一个Lambda函数。这个Lambda函数通过引用获取变量current_thread:` [&] { return current_threads; }`。变量在第21行进行递减,如果线程完成了任务,则将`active`设置为false。因此,计数器在完成阶段是递减的。 127 | 128 | 与`std::barrier`或`std::latch`相比,`std::flex_barrier`可以增加计数器。 129 | 130 | 可以在cppreference.com上阅读关于[std::latch](http://en.cppreference.com/w/cpp/experimental/latch)、[std::barrier]( http://en.cppreference.com/w/cpp/experimental/barrier)、[std::flex_barrier](http://en.cppreference.com/w/cpp/experimental/flex_barrier)的更多细节。 -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.6-chinese.md: -------------------------------------------------------------------------------- 1 | # 协程 2 | 3 | 协程是可以挂起,保持函数执行状态,并可以在之后继续执行的方式。这种方式的演化在C++中算是一种进步,协程大概率是C++20标准的一部分。 4 | 5 | 本节中介绍的C++20中的新思想,其实已经已经相当古老了。“coroutine”这个词是由[Melvin Conway](https://en.wikipedia.org/wiki/Melvin_Conway)创造的,他在1963年关于编译器的出版物中使用了这个词。[Donald Knuth](https://en.wikipedia.org/wiki/Donald_Knuth)称程序是协程的一个特例。有时候,有些想法需要一段时间才能被世人接受。 6 | 7 | C++20用两个新的关键字co_await和co_yield,扩展了C++函数的执行。 8 | 9 | co_await可以挂起表达式,如果在函数`func`中使用co_await,当调用`auto getResult = func()`不阻塞时,函数的结果不可用。不是资源消耗式的阻塞,而是资源友好式的等待。 10 | 11 | co_yield允许编写一个生成器,生成器每次返回一个新值。生成器是一种数据流,并可以从中选择相应的值。数据流可以是无限的,这样我们就可以使用C++进行惰性求值了。 12 | 13 | ## 生成器 14 | 15 | 下面的程序不太难,函数`getNumbers`返回所有的整数,从开始到结束递增为`inc`。`begin`必须小于`end`,且`inc`必须是正数。 16 | 17 | 贪婪生成器 18 | 19 | ```c++ 20 | // greedyGenerator.cpp 21 | 22 | #include 23 | #include 24 | 25 | std::vector getNumbers(int begin, int end, int inc = 1) { 26 | 27 | std::vector numbers; 28 | for (int i = begin; i < end; i += inc) { 29 | numbers.push_back(i); 30 | } 31 | 32 | return numbers; 33 | 34 | } 35 | 36 | int main() { 37 | 38 | std::cout << std::endl; 39 | 40 | const auto numbers = getNumbers(-10, 11); 41 | 42 | for (auto n : numbers) std::cout << n << " "; 43 | 44 | std::cout << "\n\n"; 45 | 46 | for (auto n : getNumbers(0, 101, 5)) std::cout << n << " "; 47 | 48 | std::cout << "\n\n"; 49 | 50 | } 51 | ``` 52 | 53 | 当然,这里用`getNumbers`重新发明轮子了,自从C++11以来,这项工作可以使用[std::iota](http://en.cppreference.com/w/cpp/algorithm/iota)来完成。 54 | 55 | 下面是输出: 56 | 57 | ![](../../../images/detail/The-Future-CPP-20-23/9.png) 58 | 59 | 对这个程序的两个观察结果比较重要:一方面,即使我只对一个有1000个元素的vector的前5个元素感兴趣,第8行的vector也会存放这1000个值。另一方面,很容易将函数`getNumbers`转换为惰性生成器。 60 | 61 | 惰性生成器 62 | 63 | ```c++ 64 | // lazyGenerator.cpp 65 | 66 | #include 67 | #include 68 | 69 | generator generatorForNumbers(int begin, int end, int inc = 1) { 70 | 71 | for (int i = begin; i < end; i += inc) { 72 | co_yield i; 73 | } 74 | 75 | } 76 | 77 | int main() { 78 | 79 | std::cout << std::endl; 80 | 81 | const auto numbers = generatorForNumbers(-10); 82 | 83 | for (int i = 1; i <= 20; ++i) std::cout << numbers << " "; 84 | 85 | std::cout << "\n\n"; 86 | 87 | for (auto n : generatorForNumbers(0, 5)) std::cout << n << " "; 88 | 89 | std::cout << "\n\n"; 90 | 91 | } 92 | ``` 93 | 94 | 当greedyGenerator.cpp中的函数`getNumbers`返回`std::vector`时,lazyGenerator.cpp中的协程`generatorForNumbers`返回生成器。第18行中的生成器编号或第24行的`generatorForNumbers(0,5)`在请求时,会返回一个新编号,并基于for循环触发查询。更准确地说,协程的查询通过`co_yield i`返回值`i`,并立即暂停执行。如果请求一个新值,协程将在该位置恢复执行。 95 | 96 | 第24行中的`generatorForNumbers(0,5)`是生成器的直接使用的一种方式。 97 | 98 | 我想强调一点,协程`generatorForNumbers`会创建无限的数据流,因为第8行中的for循环没有结束条件。如果值的数量有限(第20行)是可以的,但因为没有结束条件,第24行不会停下来,而会一直运行。 99 | 100 | 因为协程是C++添加的一个新概念,所以我想聊一聊它的细节。 101 | 102 | ## 其他细节 103 | 104 | **典型用例** 105 | 106 | 协程是编写[事件驱动应用]( https://en.wikipedia.org/wiki/Event-driven_programming)的常用方法,可以是模拟、游戏、服务器、用户界面,甚至是算法。协同程序通常用于协作的[多任务处理]( https://de.wikipedia.org/wiki/Multitasking),协作式的多任务处理的关键是,每个任务需要多少时间就花多少时间。这与抢占式的多任务形成了对比,我们可以有计划的决定每个任务占用CPU的时间。 107 | 108 | 协程还有很多种。 109 | 110 | **基础概念** 111 | 112 | C++20中的协程是不对称的、优秀的、无堆栈的。 113 | 114 | 非对称协程的工作流,会返回给调用者,这并不适用于对称协程。对称协同程序,可以将其工作流委托给另一个协同程序。 115 | 116 | 优秀的协程类似于优秀的函数,因为协序的行为类似于数据。这意味着可以将它们作为函数的参数或返回值,将它们存储在变量中。 117 | 118 | 无堆栈协程使其能够挂起,并恢复上级协同程序,但此协程不能调用另一个协程。所以,无堆栈协程通常称为可恢复函数。 119 | 120 | **设计目的** 121 | 122 | Gor Nishanov描述了协同程序的设计目的: 123 | 124 | 协程应该具有的能力: 125 | 126 | * 高度可扩展性(可到数十亿并发协程)。 127 | * 具有高效的恢复和挂起,其成本不高于函数的开销。 128 | * 与现有特性进行无缝,无开销交互。 129 | * 具有开放的协同程序机制,允许库设计人员开发使用各种高级语义(如生成器、[goroutines](https://tour.golang.org/concurrency/1)、任务等)。 130 | 131 | 由于可扩展性和与现有设施的无缝交互的设计理念,所以协同程序是无堆栈的。相反,对于堆栈式协程,在Windows上会保留默认堆栈为1MB,在Linux上会保留默认堆栈为2MB。 132 | 133 | 将函数变成协程有四种方式。 134 | 135 | **成为协程** 136 | 137 | 函数使用了协程,就变成了协程: 138 | 139 | * co_return 140 | * co_await 141 | * co_yield 142 | * co_await基于for循环的表达式。 143 | 144 | 这个解释源自提案N4628。 145 | 146 | 最后,讨论下新的关键字co_return、co_yield和co_await。 147 | 148 | **co_return , co_yield和co_await** 149 | 150 | co_return:协程使用co_return作为其返回语句。 151 | 152 | co_yield:可以实现一个生成器。这意味着可以创建一个生成器,并生成一个无限的数据流,可以连续地查询值。生成器`generator generatorForNumbers(int begin, int inc= 1)`的返回类型是`generator`。`generator`内部包含一个特殊的`promise p`,这样调用`co_yield i`就等于调用`co_await p.yield_value(i)`。`co_yield i`可以调用任意次。调用之后,协程立即暂停。 153 | 154 | co_await:会让协程挂起,并在之后恢复。`co_await exp`中的`exp`必须是可等待的表达式。`exp`必须实现一个特定的接口,这个接口由`await_ready`、`await_suspend`和`wait_resume`三个函数组成。 155 | 156 | co_await的典型用例是事件等待服务器。 157 | 158 | 阻塞式服务器 159 | 160 | ```c++ 161 | Acceptor acceptor{443}; 162 | while (true){ 163 | Socket socket= acceptor.accept(); // blocking 164 | auto request= socket.read(); // blocking 165 | auto response= handleRequest(request); 166 | socket.write(response); // blocking 167 | } 168 | ``` 169 | 170 | 这个服务器非常简单,因为会在同一个线程中依次响应每个请求。服务器监听端口443(第1行),接受连接(第3行),读取来自客户机的数据(第4行),并将应答信息传回客户机(第6行)。第3、4和6行中的所有调用都被阻塞。 171 | 172 | 由于co_await,阻塞调用现在可以暂停并恢复。 173 | 174 | 等待式服务器 175 | 176 | ```c++ 177 | Acceptor acceptor{443}; 178 | while (true){ 179 | Socket socket= co_await acceptor.accept(); 180 | auto request= co_await socket.read(); 181 | auto response= handleRequest(request); 182 | co_await socket.write(response); 183 | } 184 | ``` 185 | 186 | -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.7-chinese.md: -------------------------------------------------------------------------------- 1 | # 事务性内存 2 | 3 | 事务性内存是基于数据库理论中的事务概念。事务性内存可让使用线程变得更加容易,原因有二:第一,避免数据竞争和死锁;第二,可以组合事务。 4 | 5 | 事务具有以下属性的操作:原子性(Atomicity)、一致性(Consistency)、独立性(Isolation)和持久性(Durability)(ACID)。除了持久性和存储操作结果之外,所有的属性都适用于C++的事务性内存。现在还有三个问题。 6 | 7 | ## ACI(D) 8 | 9 | ACID是数据库事务正确执行的四个基本要素的缩写。 10 | 11 | 对于由一些语句组成的原子块,原子性、一致性和独立性意味着什么呢? 12 | 13 | 原子块 14 | 15 | ```c++ 16 | atomic{ 17 | statement1; 18 | statement2; 19 | statement3; 20 | } 21 | ``` 22 | 23 | 原子性:执行块中的所有语句或不执行块中任何语句。 24 | 25 | 一致性:系统始终处于一致的状态,所有事务确定统一的顺序。 26 | 27 | 独立性:每个事务在完全独立的情况下运行。 28 | 29 | 如何应用这些属性?事务会记住初始状态,并且在不同步的情况下执行。如果在执行过程中发生冲突,事务将中断,并恢复到初始状态,此回滚操作将再次执行事务。如果事务结束时,初始状态仍然存在,则为提交事务。冲突通常可以通过标记状态的引用来检测。 30 | 31 | 事务是一种推测行为,只有在初始状态时才会提交。与互斥锁相比,它是一种相对乐观的方法。事务在不同步的情况下执行,只有在没有冲突的情况下才会释放。互斥是一种较为悲观的方法。首先,互斥确保没有其他线程可以进入临界区。接下来,如果线程是互斥量的独占所有者,那么它将进入临界区,从而阻塞其他线程。 32 | 33 | C++以两种方式支持事务性内存:同步块和原子块。 34 | 35 | ## 同步块和原子块 36 | 37 | 目前为止,只聊了事务,现在来聊下同步块和原子块,两者可以相互封装。更具体地说,同步块不是事务,因为它们可以执行不安全事务。事务不安全的例子,类似于控制台输出的代码无法撤消。因此,同步块通常也称为自由块。 38 | 39 | **同步块** 40 | 41 | 同步块的行为就像全局锁一样,这意味着所有同步块都遵循相同的顺序,特别对同步块的所有更改,都可以在之后的同步块中使用。由于事务的提交与启动是同步的,所以在同步的块之间存在着同步关系。它们会建立一个总顺序,所以同步块不会死锁。互斥锁保护的是程序的关键区域,而同步块的则是保护整个程序。 42 | 43 | 这也就是为什么下面的程序定义良好的原因。 44 | 45 | 一个同步块 46 | 47 | ```c++ 48 | // synchronized.cpp 49 | 50 | #include 51 | #include 52 | #include 53 | 54 | int i = 0; 55 | 56 | void increment() { 57 | synchronized{ 58 | std::cout << ++i << " ,"; 59 | } 60 | } 61 | 62 | int main() { 63 | 64 | std::cout << std::endl; 65 | 66 | std::vector vecSyn(10); 67 | for (auto& thr : vecSyn) 68 | thr = std::thread([] {for (int n = 0; n < 10; ++n)increment(); }); 69 | for (auto& thr : vecSyn)thr.join(); 70 | 71 | std::cout << "\n\n"; 72 | 73 | } 74 | ``` 75 | 76 | 第7行中的变量`i`是一个全局变量,同步块中的操作是事务不安全的,但是程序是定义良好的。10个线程并发调用函数`increment`(第21行),10次增加第11行的变量`i`,对`i`和`std::cout`的访问是完全按顺序进行的,这就是同步块的特性。 77 | 78 | 程序返回预期的结果。`i`的值是按递增的顺序写的,中间用逗号隔开。下面是输出。 79 | 80 | ![](../../../images/detail/The-Future-CPP-20-23/10.png) 81 | 82 | 那么数据竞争呢?可以把它们与同步块放在一起。对源代码的一个小修改就可以引入数据竞争。 83 | 84 | 同步块的数据竞争 85 | 86 | ```c++ 87 | // nonsynchronized.cpp 88 | 89 | #include 90 | #include 91 | #include 92 | #include 93 | 94 | using namespace std::chrono_literals; 95 | 96 | 97 | int i = 0; 98 | 99 | void increment() { 100 | synchronized{ 101 | std::cout << ++i << " ,"; 102 | this_thread::sleep_for(1ns); 103 | } 104 | } 105 | 106 | int main() { 107 | 108 | std::cout << std::endl; 109 | 110 | std::vector vecSyn(10); 111 | std::vector vecUnsyn(10); 112 | 113 | for (auto& thr : vecSyn) 114 | thr = std::thread([] {for (int n = 0; n < 10; ++n)increment(); }); 115 | for (auto& thr : vecUnsyn) 116 | thr = std::thread([] {for (int n = 0; n < 10; ++n)increment(); }); 117 | 118 | for (auto& thr : vecSyn)thr.join(); 119 | for (auto& thr : vecSvecUnsynyn)thr.join(); 120 | 121 | std::cout << "\n\n"; 122 | 123 | } 124 | ``` 125 | 126 | 为了观察到数据竞争,我让同步块休眠了1纳秒(第16行)。同时,在没有没有同步块(第30行)时,访问输出流`std::cout`。总共有20个线程增加了全局变量`i`,其中一半没有同步,所以输出显示就出问题了。 127 | 128 | ![](../../../images/detail/The-Future-CPP-20-23/11.png) 129 | 130 | 我在有输出的问题的输出周围画上红色的圆圈。这些是`std::cout`由至少两个线程同时写入的位置。C++11保证字符是自动编写的,而这并不是问题的原因。更糟糕的是,变量`i`是由多于两个线程进行修改的,这就是一场数据竞赛。因此,程序会出现未定义行为。计数器的最终结果应该是200,但结果是199。这意味着,计数中有值被覆盖了。 131 | 132 | 同步块的顺序也适用于原子块。 133 | 134 | **原子块** 135 | 136 | 可以在同步块中执行事务不安全代码,但不能在原子块中执行。原子块有三种形式:`atomic_noexcept`、`atomic_commit`和`atomic_cancel`。三个后缀`_noexcept`、`_commit`和`_cancel`定义了原子块如何对异常进行管理: 137 | 138 | atomic_noexcept:如果抛出异常,将调用`std::abort`中止程序。 139 | 140 | atomic_cancel:默认情况下,会调用`std::abort`。如果抛出一个终止事务的安全异常,则不存在这种情况。在这种情况下,事务将取消,并进入初始状态并抛出异常。 141 | 142 | atomic_commit:如果抛出异常,则提交事务。 143 | 144 | 具有事务安全异常的有: [std::bad_alloc](http://en.cppreference.com/w/cpp/memory/new/bad_alloc), [std::bad_array_length]( https://www.cs.helsinki.fi/group/boi2016/doc/cppreference/reference/en.cppreference.com/w/cpp/memory/new/bad_array_length.html), [std::bad_array_new_length](http://en.cppreference.com/w/cpp/memory/new/bad_array_new_length), [std::bad_cast](http://en.cppreference.com/w/cpp/types/bad_cast), [std::bad_typeid](http://en.cppreference.com/w/cpp/types/bad_typeid), [std::bad_exception](http://en.cppreference.com/w/cpp/error/bad_exception), [std::exception](http://en.cppreference.com/w/cpp/error/exception), 以及所有(从这些异常中)派生出来的异常。 145 | 146 | ## transaction_safe与transaction_unsafe的代码比较 147 | 148 | 可以将函数声明为transaction_safe,或者将transaction_unsafe属性附加到它。 149 | 150 | transaction_safe与transaction_unsafe 151 | 152 | ```c++ 153 | int transactionSafeFunction() transaction_safe; 154 | 155 | [[transaction_unsafe]] int transactionUnsafeFunction(); 156 | ``` 157 | 158 | transaction_safe属于函数类型,但transaction_safe是什么意思?根据[N4265]( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4265.html), transaction_safe函数是一个具有transaction_safe定义的函数。如果不出现下列属性定义,则该定义成立: 159 | 160 | * 有volatile参数或变量。 161 | * 有事务不安全的语句。 162 | * 当函数体中使用一个类的构造和析构函数,而这个类具有volatile的非静态成员。 163 | 164 | 当然,这个transaction_safe定义是不稳定的,你可以阅读提案[N4265]( http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4265.html) ,了解更多细节。 165 | 166 | -------------------------------------------------------------------------------- /content/The-Details/The-Future-CPP-20-23/5.8-chinese.md: -------------------------------------------------------------------------------- 1 | # 任务块 2 | 3 | 任务块使用fork-join范型来并行执行任务,其已经是[C++扩展并行性2版技术规范](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/n4742.html)的一部分。因此,我们很有可能在C++20中看到它们。 4 | 5 | 谁在C++中发明了任务块?微软的[Parallel Patterns Library (PPL)](https://en.wikipedia.org/wiki/Parallel_Patterns_Library)和英特尔的[Threading Building Blocks (TBB)](https://en.wikipedia.org/wiki/Threading_Building_Blocks)都参与了[N4441提案](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4411.pdf)。另外,Intel使用了他们的[Cilk Plus语言库](https://en.wikipedia.org/wiki/Cilk)。 6 | 7 | fork-join这个很容易理解。 8 | 9 | ## Fork和Join 10 | 11 | 解释fork-join范式最直接的方法是使用图形。 12 | 13 | ![](../../../images/detail/The-Future-CPP-20-23/12.png) 14 | 15 | 它是如何工作的? 16 | 17 | 创建者调用`define_task_block`或`define_task_block_restore_thread`,此调用会创建一个任务块,该任务块可以创建任务,也可以等待任务完成,同步位于任务块的末尾。创建新任务是fork阶段,任务块的同步是工作流的联接阶段,这只是一个简单的描述。让我们来看一段代码。 18 | 19 | 定义一个任务块 20 | 21 | ```c++ 22 | template 23 | int traverse(node& n, Func &&f){ 24 | int left = 0, right = 0; 25 | define_task_block( 26 | [&](task_block& tb){ 27 | if (n.left) tb.run([&]{left = traverse(*n.left, f);}); 28 | if (n.right) tb.run([&]{right = traverse(*n.right, f);}); 29 | } 30 | ); 31 | return f(n) + left + right; 32 | } 33 | ``` 34 | 35 | traverse是一个函数模板,它在树的每个节点上调用函数`f`。关键字`define_task_block`定义了任务块,任务块`tb`可以在任务块中启动一个新任务,这发生在第6行和第7行树的左右分支上。第9行是任务块的末端,因此是同步点。 36 | 37 | > **HPX(高性能ParalleX)** 38 | > 39 | > 上面的例子来自[HPX (High-Performance ParalleX)](http://stellar.cct.lsu.edu/projects/hpx/)框架的文档,它是一个通用的C++运行时,适用于任何规模的并行和分布式应用程序。HPX已经实现了许多本章介绍的,即将发布的C++ 20/23标准中的特性。 40 | 41 | 可以使用`define_task_block`函数或`define_task_block_restore_thread`函数定义一个任务块。 42 | 43 | ## define_task_block与define_task_block_restore_thread 44 | 45 | 区别在于,`define_task_block_restore_thread`函数保证任务块的创建者线程与任务块完成后运行的线程是相同的,而`define_task_block`函数则相反。 46 | 47 | define_task_block与define_task_block_restore_thread 48 | 49 | ```c++ 50 | ... 51 | define_task_block([&](auto& tb){ 52 | tb.run([&]{[]fun();}); 53 | define_task_block_restore_thread([&](auot& tb){ 54 | tb.run([&]{[]{func2();}); 55 | define_task_block([&](auto& tb){ 56 | tb.run([&]{func3();}); 57 | }); 58 | ... 59 | ... 60 | }); 61 | ... 62 | ... 63 | }); 64 | ... 65 | ... 66 | ``` 67 | 68 | 任务块确保最外层任务块(第2 - 14行)的创建者线程,与完成任务块后运行语句的线程完全相同。这意味着执行第2行的线程与执行第15和16行的线程相同。这种保证不适用于嵌套的任务块,第6 - 8行任务块的创建者线程不会自动执行第9行和第10行。现在执行第4行的创建者线程与执行第12行和第13行的线程是相同的,如果需要嵌套,则应该使用define_task_block_restore_thread函数(第4行)。 69 | 70 | ## 接口 71 | 72 | 任务块的接口非常有限,不能构造、销毁、复制或移动task_block类的实例。只能对其使用define_task_block函数或define_task_block_restore_thread函数。`task_block tb`在定义的任务块范围内活动,因此可以启动新任务(`tb.run`)或等待(`tb.wait`)直到任务完成。 73 | 74 | 任务块的最小接口 75 | 76 | ```c++ 77 | define_task_block([&](auto& tb){ 78 | tb.run([&]{process(x1, x2)}); 79 | if(x2==x3) tb.wait(); 80 | process(x3, x4); 81 | }); 82 | ``` 83 | 84 | 这段代码在做什么呢?第2行启动了一个新任务,这个任务需要数据`x1`和`x2`才能进行,第4行使用数据`x3`和`x4`。如果`x2 == x3`为真,则必须保护变量不受共享访问。这就是任务块`tb`等待第2行任务完成的原因。 85 | 86 | 如果函数`task_block::run`或`task_block::wait`检测到当前任务块中有异常,则会抛出一个类似于`task_cancelled_exception`的异常。 87 | 88 | ## 调度器 89 | 90 | 调度器管理线程运行,这意味着决定谁执行任务不再是程序开发者的责任。线程只是一个实现细节。 91 | 92 | 执行新创建的任务有两种策略。父线程表示创建者线程,子线程表示新任务。 93 | 94 | 窃取子任务:调度程序窃取其任务并执行它。 95 | 96 | 窃取父任务:现在调度器窃取任务块`tb`本身执行任务。 97 | 98 | [提案N4441](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4411.pdf)支持这两种策略。 99 | 100 | -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/cover.jpg -------------------------------------------------------------------------------- /images/Further-Information/Challenges/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/1.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/10.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/2.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/3.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/4.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/5.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/6.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/7.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/8.png -------------------------------------------------------------------------------- /images/Further-Information/Challenges/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/Challenges/9.png -------------------------------------------------------------------------------- /images/Further-Information/CppMem/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/CppMem/1.png -------------------------------------------------------------------------------- /images/Further-Information/CppMem/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/CppMem/2.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/1.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/10.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/2.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/3.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/4.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/5.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/6.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/7.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/8.png -------------------------------------------------------------------------------- /images/Further-Information/The-Time-Library/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Further-Information/The-Time-Library/9.png -------------------------------------------------------------------------------- /images/History-Quick-Overview/0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/History-Quick-Overview/0.png -------------------------------------------------------------------------------- /images/History-Quick-Overview/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/History-Quick-Overview/1.png -------------------------------------------------------------------------------- /images/History-Quick-Overview/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/History-Quick-Overview/2.png -------------------------------------------------------------------------------- /images/History-Quick-Overview/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/History-Quick-Overview/3.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/1.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/10.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/2.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/3.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/4.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/5.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/6.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/7.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/8.png -------------------------------------------------------------------------------- /images/Patterns/Best-Practices/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Best-Practices/9.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/1.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/2.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/3.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/4.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/5.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/6.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/7.png -------------------------------------------------------------------------------- /images/Patterns/Concurrent-Architecture/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Concurrent-Architecture/8.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/1.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/10.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/11.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/12.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/2.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/3.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/4.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/5.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/6.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/7.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/8.png -------------------------------------------------------------------------------- /images/Patterns/Synchronisation-Patterns/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/Patterns/Synchronisation-Patterns/9.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/10.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/11.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/12.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/13.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/14.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/15.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/16.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/17.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/18.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/19.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/20.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/21.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/22.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/23.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/24.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/25.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/26.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/27.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/28.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/29.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/30.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/31.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/32.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/33.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/34.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/35.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/35.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/36.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/37.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/37.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/38.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/39.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/39.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/40.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/41.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/42.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/43.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/44.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/45.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/46.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/47.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/48.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/49.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/50.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/51.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/52.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/53.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/54.png -------------------------------------------------------------------------------- /images/detail/Case-Studies/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Case-Studies/55.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/1.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/2.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/3.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/4.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/5.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/6.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/7.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/8.png -------------------------------------------------------------------------------- /images/detail/Parallel-Algorithms-of-the-Standard/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/Parallel-Algorithms-of-the-Standard/9.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/1.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/10.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/11.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/12.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/2.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/3.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/4.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/5.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/6.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/7.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/8.png -------------------------------------------------------------------------------- /images/detail/The-Future-CPP-20-23/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/The-Future-CPP-20-23/9.png -------------------------------------------------------------------------------- /images/detail/memory-model/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/1.png -------------------------------------------------------------------------------- /images/detail/memory-model/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/10.png -------------------------------------------------------------------------------- /images/detail/memory-model/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/11.png -------------------------------------------------------------------------------- /images/detail/memory-model/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/12.png -------------------------------------------------------------------------------- /images/detail/memory-model/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/13.png -------------------------------------------------------------------------------- /images/detail/memory-model/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/14.png -------------------------------------------------------------------------------- /images/detail/memory-model/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/15.png -------------------------------------------------------------------------------- /images/detail/memory-model/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/16.png -------------------------------------------------------------------------------- /images/detail/memory-model/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/17.png -------------------------------------------------------------------------------- /images/detail/memory-model/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/18.png -------------------------------------------------------------------------------- /images/detail/memory-model/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/19.png -------------------------------------------------------------------------------- /images/detail/memory-model/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/2.png -------------------------------------------------------------------------------- /images/detail/memory-model/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/20.png -------------------------------------------------------------------------------- /images/detail/memory-model/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/21.png -------------------------------------------------------------------------------- /images/detail/memory-model/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/22.png -------------------------------------------------------------------------------- /images/detail/memory-model/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/23.png -------------------------------------------------------------------------------- /images/detail/memory-model/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/24.png -------------------------------------------------------------------------------- /images/detail/memory-model/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/25.png -------------------------------------------------------------------------------- /images/detail/memory-model/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/26.png -------------------------------------------------------------------------------- /images/detail/memory-model/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/27.png -------------------------------------------------------------------------------- /images/detail/memory-model/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/28.png -------------------------------------------------------------------------------- /images/detail/memory-model/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/29.png -------------------------------------------------------------------------------- /images/detail/memory-model/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/3.png -------------------------------------------------------------------------------- /images/detail/memory-model/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/30.png -------------------------------------------------------------------------------- /images/detail/memory-model/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/31.png -------------------------------------------------------------------------------- /images/detail/memory-model/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/32.png -------------------------------------------------------------------------------- /images/detail/memory-model/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/4.png -------------------------------------------------------------------------------- /images/detail/memory-model/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/5.png -------------------------------------------------------------------------------- /images/detail/memory-model/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/6.png -------------------------------------------------------------------------------- /images/detail/memory-model/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/7.png -------------------------------------------------------------------------------- /images/detail/memory-model/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/8.png -------------------------------------------------------------------------------- /images/detail/memory-model/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/memory-model/9.png -------------------------------------------------------------------------------- /images/detail/multithreading/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/1.png -------------------------------------------------------------------------------- /images/detail/multithreading/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/10.png -------------------------------------------------------------------------------- /images/detail/multithreading/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/11.png -------------------------------------------------------------------------------- /images/detail/multithreading/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/12.png -------------------------------------------------------------------------------- /images/detail/multithreading/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/13.png -------------------------------------------------------------------------------- /images/detail/multithreading/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/14.png -------------------------------------------------------------------------------- /images/detail/multithreading/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/15.png -------------------------------------------------------------------------------- /images/detail/multithreading/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/16.png -------------------------------------------------------------------------------- /images/detail/multithreading/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/17.png -------------------------------------------------------------------------------- /images/detail/multithreading/18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/18.png -------------------------------------------------------------------------------- /images/detail/multithreading/19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/19.png -------------------------------------------------------------------------------- /images/detail/multithreading/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/2.png -------------------------------------------------------------------------------- /images/detail/multithreading/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/20.png -------------------------------------------------------------------------------- /images/detail/multithreading/21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/21.png -------------------------------------------------------------------------------- /images/detail/multithreading/22.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/22.png -------------------------------------------------------------------------------- /images/detail/multithreading/23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/23.png -------------------------------------------------------------------------------- /images/detail/multithreading/24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/24.png -------------------------------------------------------------------------------- /images/detail/multithreading/25.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/25.png -------------------------------------------------------------------------------- /images/detail/multithreading/26.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/26.png -------------------------------------------------------------------------------- /images/detail/multithreading/27.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/27.png -------------------------------------------------------------------------------- /images/detail/multithreading/28.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/28.png -------------------------------------------------------------------------------- /images/detail/multithreading/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/29.png -------------------------------------------------------------------------------- /images/detail/multithreading/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/3.png -------------------------------------------------------------------------------- /images/detail/multithreading/30.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/30.png -------------------------------------------------------------------------------- /images/detail/multithreading/31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/31.png -------------------------------------------------------------------------------- /images/detail/multithreading/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/32.png -------------------------------------------------------------------------------- /images/detail/multithreading/33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/33.png -------------------------------------------------------------------------------- /images/detail/multithreading/34.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/34.png -------------------------------------------------------------------------------- /images/detail/multithreading/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/4.png -------------------------------------------------------------------------------- /images/detail/multithreading/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/5.png -------------------------------------------------------------------------------- /images/detail/multithreading/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/6.png -------------------------------------------------------------------------------- /images/detail/multithreading/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/7.png -------------------------------------------------------------------------------- /images/detail/multithreading/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/8.png -------------------------------------------------------------------------------- /images/detail/multithreading/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xiaoweiChen/Concurrency-with-Modern-Cpp/d6dff1a1f7d8c91c9a3f36ee9975bfe4040e9ab6/images/detail/multithreading/9.png --------------------------------------------------------------------------------