8.0-chinese
第8章 并发代码设计本章主要内容 线程间划分数据的技术 影响并发代码性能的因素 性能因素是如何影响数据结构的设计 多线程代码中的异常安全 可扩展性 并行算法的实现 之前章节着重于介绍使用C++11中的新工具来写并发代码。在第6、7章中我们了解到,如何使用这些工具来设计可并发访问的基本数据结构。这就好比一个木匠,其不仅要知道如何做一个合页,一个组合柜,或一个桌子;并发的代码的使用,要比使用/设计基本数据结构频繁的多。要将眼界放宽,就需要构建更大的结构,进行高效的工作。我将使用多线程化的C++标准库算法作为例子,不过这里的原则也适用于对其他应用程序的扩展。 认真思考如何进行并发化设计,对于每个编程项目来说都很重要。不过,写多线程代码的时候,需要考虑的因素比写序列化代码多得多。不仅包括一般性因素,例如:封装,耦合和聚合(这些在很多软件设计书籍中有很详细的介绍),还要考虑哪些数据需要共享,如何同步访问数据,哪些线程需要等待哪些线程,等等。 本章将会关注这些问题,从高层(但也是基本的)考虑,如何使用线程,哪些代码应该在哪些线程上执行;以及,这将如何影响代码的清晰度,并从底层细...
8.1-chinese
8.1 线程间划分工作的技术试想,你被要求负责建造一座房子。为了完成任务,你需要挖地基、砌墙、添加水暖、接入电线,等等。理论上,如果你很擅长建造屋子,那么这些事情都可以由你来完成,但是这样就要花费很长很长时间,并且需要不断的切换任务。或者,你可以雇佣一些人来帮助你完成房子的建造。那么现在你需要决定雇多少人,以及雇佣人员具有什么样的技能。比如,你可以雇几个人,这几个人什么都会。现在你还得不断的切换任务,不过因为雇佣了很多人,就要比之前的速度快很多。 或者,你可以雇佣一个包工队(专家组),由瓦工,木匠,电工和水管工组成。你的包工队员只做其擅长的,所以当没有水暖任务时,水管工会坐在那里休息,喝茶或咖啡。因为人多的缘故,要比之前一个人的速度快很多,并且水管工在收拾厕所的时候,电工可以将电线连接到厨房,不过当没有属于自己的任务时,有人就会休息。即使有人在休息,你可能还是能感觉到包工队要比雇佣一群什么都会的人快。包工队不需要更换工具,并且每个人的任务都要比会的人做的快。是快还是慢,取决于特定的情况——需要尝试,进行观察。 即使雇佣包工队,你依旧可以选择人数不同的团队(可能在一个团队中,瓦工的...
8.2-chinese
8.2 影响并发代码性能的因素`多处理系统中,使用并发的方式来提高代码的效率时,你需要了解一下有哪些因素会影响并发的效率。即使已经使用多线程对关注进行分离,还需要确定是否会对性能造成负面影响。因为,在16核机器上应用的速度与单核机器相当时,用户是不会打死你的。 之后你会看到,在多线程代码中有很多因素会影响性能——对线程处理的数据做一些简单的改动(其他不变),都可能对性能产生戏剧性的效果。所以,多言无益,让我们来看一下这些因素吧,从明显的开始:目标系统有多少个处理器? 8.2.1 有多少个处理器?处理器个数是影响多线程应用的首要因素。在某些情况下,你对目标硬件会很熟悉,并且针对硬件进行设计,并在目标系统或副本上进行测量。如果是这样,那你很幸运;不过,要知道这些都是很奢侈的。你可能在一个类似的平台上进行开发,不过你所使用的平台与目标平台的差异很大。例如,你可能会在一个双芯或四芯的系统上做开发,不过你的用户系统可能就只有一个处理器(可能有很多芯),或多个单芯处理器,亦或是多核多芯的处理器。在不同的平台上,并发程序的行为和性能特点就可能完全不同,所以你需要仔细考虑那些地方会被影响...
8.3-chinese
8.3 为多线程性能设计数据结构8.1节中,我们看到了各种划分方法;并且在8.2节,了解了对性能影响的各种因素。如何在设计数据结构的时候,使用这些信息提高多线程代码的性能?这里的问题与第6、7章中的问题不同,之前是关于如何设计能够安全、并发访问的数据结构。在8.2节中,单线程中使用的数据布局就会对性能产生巨大冲击(即使数据并未与其他线程进行共享)。 关键的是,当为多线程性能而设计数据结构的时候,需要考虑竞争(contention),伪共享(false sharing)和数据距离(data proximity)。这三个因素对于性能都有着重大的影响,并且你通常可以改善的是数据布局,或者将赋予其他线程的数据元素进行修改。首先,让我们来看一个轻松方案:线程间划分数组元素。 8.3.1 为复杂操作划分数组元素假设你有一些偏数学计算任务,比如,需要将两个很大的矩阵进行相乘。对于矩阵相乘来说,将第一个矩阵中的首行每个元素和第二个矩阵中首列每个元素相乘后,再相加,从而产生新矩阵中左上角的第一个元素。然后,第二行和第一列,产生新矩阵第一列上的第二个结果,第二行和第二列,产生新矩阵中第二列的第一个结...
8.4-chinese
8.4 设计并发代码的注意事项目前为止,在本章中我们已经看到了很多线程间划分工作的方法,影响性能的因素,以及这些因素是如何影响你选择数据访问模式和数据结构的。虽然,已经有了很多设计并发代码的内容。你还需要考虑很多事情,比如异常安全和可扩展性。随着系统中核数的增加,性能越来越高(无论是在减少执行时间,还是增加吞吐率),这样的代码称为“可扩展”代码。理想状态下,性能随着核数的增加线性增长,也就是当系统有100个处理器时,其性能是系统只有1核时的100倍。 虽然,非扩展性代码依旧可以正常工作——单线程应用就无法扩展——例如,异常安全是一个正确性问题。如果你的代码不是异常安全的,最终会破坏不变量,或是造成条件竞争,亦或是你的应用意外终止,因为某个操作会抛出异常。有了这个想法,我们就率先来看一下异常安全的问题。 8.4.1 并行算法中的异常安全异常安全是衡量C++代码一个很重要的指标,并发代码也不例外。实际上,相较于串行算法,并行算法常会格外要求注意异常问题。当一个操作在串行算法中抛出一个异常,算法只需要考虑对其本身进行处理,以避免资源泄露和损坏不变量;这里可以允许异常传递给调用者,由调用...
8.5-chinese
8.5 在实践中设计并发代码当为一个特殊的任务设计并发代码时,需要根据任务本身来考虑之前所提到的问题。为了展示以上的注意事项是如何应用的,我们将看一下在C++标准库中三个标准函数的并行实现。当你遇到问题时,这里的例子可以作为很好的参照。在有较大的并发任务进行辅助下,我们也将实现一些函数。 我主要演示这些实现使用的技术,不过可能这些技术并不是最先进的;更多优秀的实现可以更好的利用硬件并发,不过这些实现可能需要到与并行算法相关的学术文献,或者是多线程的专家库中(比如:Inter的TBB[4])才能看到。 并行版的std::for_each可以看作为能最直观体现并行概念,就让我们从并行版的std::for_each开始吧! 8.5.1 并行实现:std::for_eachstd::for_each的原理很简单:其对某个范围中的元素,依次调用用户提供的函数。并行和串行调用的最大区别就是函数的调用顺序。std::for_each是对范围中的第一个元素调用用户函数,接着是第二个,以此类推,而在并行实现中对于每个元素的处理顺序就不能保证了,并且它们可能(我们希望如此)被并发的处理。 为了实现这...
8.6-chinese
8.6 本章总结本章我们讨论了很多东西。我们从划分线程间的工作开始(比如,数据提前划分或让线程形成流水线)。之后,以低层次视角来看多线程下的性能问题,顺带了解了伪共享和数据通讯;了解访问数据的模式对性能的影响。再后,了解了附加注意事项是如何影响并发代码设计的,比如:异常安全和可扩展性。最后,用一些并行算法实现来结束了本章,在设计这些并行算法实现时碰到的问题,在设计其他并行代码的时候也会遇到。 本章中,关于线程池的部分被转移了。线程池——一个预先设定的线程组,会将任务指定给池中的线程。很多不错的想法可以用来设计一个不过的线程池;所以我们将在下一章中来看一些有关线程池的问题,以及高级线程管理方式。
9.1-chinese
9.1 线程池很多公司里,雇员通常会在办公室度过他们的办公时光(偶尔也会外出访问客户或供应商),或是参加贸易展会。虽然外出可能很有必要,并且可能需要很多人一起去,不过对于一些特别的雇员来说,一趟可能就是几个月,甚至是几年。公司要给每个雇员都配一辆车,这基本上是不可能的,不过公司可以提供一些共用车辆;这样就会有一定数量车,来让所有雇员使用。当一个员工要去异地旅游时,那么他就可以从共用车辆中预定一辆,并在返回公司的时候将车交还。如果某天没有闲置的共用车辆,雇员就得不延后其旅程了。 线程池就是类似的一种方式,在大多数系统中,将每个任务指定给某个线程是不切实际的,不过可以利用现有的并发性,进行并发执行。线程池就提供了这样的功能,提交到线程池中的任务将并发执行,提交的任务将会挂在任务队列上。队列中的每一个任务都会被池中的工作线程所获取,当任务执行完成后,再回到线程池中获取下一个任务。 创建一个线程池时,会遇到几个关键性的设计问题,比如:可使用的线程数量,高效的任务分配方式,以及是否需要等待一个任务完成。 在本节,我们将看到线程池是如何解决这些问题的,从最简单的线程池开始吧! 9.1.1 最...
9.2-chinese
9.2 中断线程很多情况下,使用信号来终止一个长时间运行的线程是合理的。这种线程的存在,可能是因为工作线程所在的线程池被销毁,或是用户显式的取消了这个任务,亦或其他各种原因。不管是什么原因,原理都一样:需要使用信号来让未结束线程停止运行。这里需要一种合适的方式让线程主动的停下来,而非让线程戛然而止。 你可能会给每种情况制定一个独立的机制,这样做的意义不大。不仅因为用统一的机制会更容易在之后的场景中实现,而且写出来的中断代码不用担心在哪里使用。C++11标准没有提供这样的机制,不过实现这样的机制也并不困难。 在了解一下应该如何实现这种机制前,先来了解一下启动和中断线程的接口。 9.2.1 启动和中断线程先看一下外部接口,需要从可中断线程上获取些什么?最起码需要和std::thread相同的接口,还要多加一个interrupt()函数: 12345678910class interruptible_thread{public: template<typename FunctionType> interruptible_thread(FunctionType ...
9.0-chinese
第9章 高级线程管理本章主要内容 线程池 处理线程池中任务的依赖关系 池中线程如何获取任务 中断线程 之前的章节中,我们通过创建std::thread对象来对线程进行管理。在一些情况下,这种方式不可行了,因为需要在线程的整个生命周期中对其进行管理,并根据硬件来确定线程数量,等等。理想情况是将代码划分为最小块,再并发执行,之后交给处理器和标准库,进行性能优化。 另一种情况是,当使用多线程来解决某个问题时,在某个条件达成的时候,可以提前结束。可能是因为结果已经确定,或者因为产生错误,亦或是用户执行终止操作。无论是哪种原因,线程都需要发送“请停止”请求,放弃任务,清理,然后尽快停止。 本章,我们将了解一下管理线程和任务的机制,从自动管理线程数量和自动管理任务划分开始。












