本章主要内容线程池处理线程池中任务的依赖关系池中线程如何获取任务中断线程之前的章节中,我们通过创建std::thread对象来对线程进行管理。在一些情况下,这种方式不可行了,因为需要在线程的整个生命周期中对其进行管理,并根据硬件来确定线程数量,等等。理想情况是将代码划分为最小块,再并发执行,之后交给处理器和标准库,进行性能优化。
本章我们讨论了很多东西。我们从划分线程间的工作开始(比如,数据提前划分或让线程形成流水线)。之后,以低层次视角来看多线程下的性能问题,顺带了解了伪共享和数据通讯;了解访问数据的模式对性能的影响。再后,了解了附加注意事项是如何影响并发代码设计的,比如:异常安全和可扩展性。
当为一个特殊的任务设计并发代码时,需要根据任务本身来考虑之前所提到的问题。为了展示以上的注意事项是如何应用的,我们将看一下在C++标准库中三个标准函数的并行实现。当你遇到问题时,这里的例子可以作为很好的参照。在有较大的并发任务进行辅助下,我们也将实现一些函数。我主要演示这些实现使用的技术,不过可能这些技术并不是最先进的;
目前为止,在本章中我们已经看到了很多线程间划分工作的方法,影响性能的因素,以及这些因素是如何影响你选择数据访问模式和数据结构的。虽然,已经有了很多设计并发代码的内容。你还需要考虑很多事情,比如异常安全和可扩展性。随着系统中核数的增加,性能越来越高(无论是在减少执行时间,还是增加吞吐率),这样的代码称为“可扩展”代码。
8.1节中,我们看到了各种划分方法;并且在8.2节,了解了对性能影响的各种因素。如何在设计数据结构的时候,使用这些信息提高多线程代码的性能?这里的问题与第6、7章中的问题不同,之前是关于如何设计能够安全、并发访问的数据结构。在8.2节中,单线程中使用的数据布局就会对性能产生巨大冲击(即使数据并未与其他线程进行共享)。
`多处理系统中,使用并发的方式来提高代码的效率时,你需要了解一下有哪些因素会影响并发的效率。即使已经使用多线程对关注进行分离,还需要确定是否会对性能造成负面影响。因为,在16核机器上应用的速度与单核机器相当时,用户是不会打死你的。
试想,你被要求负责建造一座房子。为了完成任务,你需要挖地基、砌墙、添加水暖、接入电线,等等。理论上,如果你很擅长建造屋子,那么这些事情都可以由你来完成,但是这样就要花费很长很长时间,并且需要不断的切换任务。或者,你可以雇佣一些人来帮助你完成房子的建造。那么现在你需要决定雇多少人,以及雇佣人员具有什么样的技能。比如,你可以雇几个人,这几个人什么都会。
本章主要内容线程间划分数据的技术影响并发代码性能的因素性能因素是如何影响数据结构的设计多线程代码中的异常安全可扩展性并行算法的实现之前章节着重于介绍使用C++11中的新工具来写并发代码。在第6、7章中我们了解到,如何使用这些工具来设计可并发访问的基本数据结构。这就好比一个木匠,其不仅要知道如何做一个合页,一个组合柜,或一个桌子;
从第6章中的基于锁的数据结构起,本章简要的描述了一些无锁数据结构的实现(通过实现栈和队列)。在这个过程中,需要小心使用原子操作的内存序,为了保证无数据竞争,以及让每个线程看到一个预制相关的数据结构。也能了解到,在无锁结构中对内存的管理是越来越难。还有,如何通过帮助线程的方式,来避免忙等待循环。设计无锁数据结构是一项很困难的任务,并且很容易犯错;
本章中的例子中,看到了一些复杂的代码可让无锁结构工作正常。如果要设计自己的数据结构,一些指导建议可以帮助你找到设计重点。第6章中关于并发通用指导建议还适用,不过这里需要更多的建议。我从例子中抽取了几个实用的指导建议,在你设计无锁结构数据的时候就可以直接引用。7.3.
为了演示一些在设计无锁数据结构中所使用到的技术,我们将看到一些无锁实现的简单数据结构。这里不仅要在每个例子中描述一个有用的数据结构实现,还将使用这些例子的某些特别之处来阐述对于无锁数据结构的设计。如之前所提到的,无锁结构依赖与原子操作和内存序及相关保证,以确保多线程以正确的顺序访问数据结构。
使用互斥量、条件变量,以及“期望”来同步阻塞数据的算法和数据结构。应用调用库函数,将会挂起一个执行线程,直到其他线程完成某个特定的动作。库函数将调用阻塞操作来对线程进行阻塞,在阻塞移除前,线程无法继续自己的任务。通常,操作系统会完全挂起一个阻塞线程(并将其时间片交给其他线程),直到其被其他线程“解阻塞”;
本章主要内容设计无锁并发数据结构无锁结构中内存管理技术对无锁数据结构设计的简单指导上一章中,我们了解了在设计并发数据结构时会遇到的问题,根据指导意见指引,确定设计的安全性。对一些通用数据结构进行检查,并查看使用互斥锁对共享数据进行保护的实现例子。
本章开篇,我们讨论了设计并发数据结构的意义,以及给出了一些指导意见。然后,通过设计一些通用的数据结构(栈,队列,哈希表和单链表),探究了在指导意见在实现这些数据结构的应用,并使用锁来保护数据和避免数据竞争。那么现在,你应该回看一下本章实现的那些数据结构,再回顾一下如何增加并发访问的几率,和哪里会存在潜在条件竞争。
栈和队列都很简单:接口相对固定,并且它们应用于比较特殊的情况。并不是所有数据结构都像它们一样简单;大多数数据结构支持更加多样化的操作。原则上,这将增大并行的可能性,但是也让对数据保护变得更加困难,因为要考虑对所有能访问到的部分。当为了并发访问对数据结构进行设计时,这一系列原有的操作,就变得越发重要,需要重点处理。
基于锁的并发数据结构设计,需要确保访问线程持有锁的时间最短。对于只有一个互斥量的数据结构来说,这十分困难。需要保证数据不被锁之外的操作所访问到,并且还要保证不会在固有结构上产生条件竞争(如第3章所述)。当你使用多个互斥量来保护数据结构中不同的区域时,问题会暴露的更加明显,当操作需要获取多个互斥锁时,就有可能产生死锁。
设计并发数据结构,意味着多个线程可以并发的访问这个数据结构,线程可对这个数据结构做相同或不同的操作,并且每一个线程都能在自己的自治域中看到该数据结构。且在多线程环境下,无数据丢失和损毁,所有的数据需要维持原样,且无条件竞争。这样的数据结构,称之为“线程安全”的数据结构。
本章主要内容并发数据结构设计的意义指导如何设计实现为并发设计的数据结构在上一章中,我们对底层原子操作和内存模型有了详尽的了解。在本章中,我们将先将底层的东西放在一边(将会在第7章再次提及),来对数据结构做一些讨论。数据结构的选择,对于程序来说,是其解决方案的重要组成部分,当然,并行程序也不例外。
在本章中,已经对C++11内存模型的底层只是进行详尽的了解,并且了解了原子操作能在线程间提供基本的同步。这里包含基本的原子类型,由std::atomic<>类模板特化后提供;接口,以及对于这些类型的操作,还要有对内存序列选项的各种复杂细节,都由原始std::atomic<>类模板提供。我们也了解了栅栏,了解其如何让执行序列中,对原子类型的操作同步成对。
假设你有两个线程,一个向数据结构中填充数据,另一个读取数据结构中的数据。为了避免恶性条件竞争,第一个线程设置一个标志,用来表明数据已经准备就绪,并且第二个线程在这个标志设置前不能读取数据。下面的程序清单就是这样的情况。清单5.2 不同线程对数据的读写#include <vector>#include <atomic>#include <iostream>std::vector<int>
关注时代Java