最近正好在创思学习多线程,对刘老师讲解的那几个PC问题记忆深刻,随机翻了翻并发编程的书籍,收益颇多,最近很少空间写东西,就给大家推荐本书吧,《JAVA并发编程实践》不像其他的书到处是代码,深入浅出的说明了并发编程的趋势和用法。
其实并行编程对我们来说也不是什么难问题,但是我们考虑问题一般都是以原子操作为前提,所以我们更加注意的是在某一段时间内的工作地串行化,所以并发更受喜爱,也比较经典,各有千秋吧。 一、Amdahl定律概述 计算机科学中的一个重要定律。 描述:系统优化某部件所获得的系统性能的改善程度,取决于该部件被使用的频率,或所占总执行时间的比例。 主要应用:改善“系统瓶颈”性能。 Amdahl定律定义了加速比: 加速比=采用改进措施后性能/为采用改进措施前的性能 =未采用改进措施前执行某任务时间/采用改进措施后执行某任务的时间 n个处理器加速因子S=n/[1+(n-1)f]:f为非平行百分比,n越大,S不能超过1/f 二、Amdahl定律 有些问题使用越多的资源就能越快地解决——越多的工人参与收割庄稼,那么就能越快地完 成收获。另一些任务根本就是串行化的——增加更多的工人根本不可能提高收割速度。如果我们使用线程的重要原因之一是为了支配多处理器的能力,我们必须保证 问题被恰当地进行了并行化的分解,并且我们的程序有效地使用了这种并行的潜能。 大多数并发程序都与农耕有着很多相似之处,由一系列并行和串行化的片断组成。Amdahl定律描述了在一个系统中,基于可并行化和串行化的组件各自所占的比重,程序通过获得额外的计算资源,理论上能够加速多少。如果F是必须串行化执行的比重,那么Amdahl定律告诉我们,在一个N处理器的机器中,我们最多可以加速: 当N无限增大趋近无穷时,speedup的最大值无限趋近1/F,这意味着一个程序中如果50%的处理都需要串行进行的话,speedup只能提升2倍(不考虑事实上有多少线程可用);如果程序的10%需要串行进行,speedup最 多能够提高近10倍。Amdahl定律同样量化了串行化的效率开销。在拥有10个处理器的系统中,程序如果有10%是串行化的,那么最多可以加速5.3倍 (53%的使用率),在拥有100个处理器的系统中,这个数字可以达到9.2(9%的使用率)。这使得无效的CPU利用永远不可能到达10倍。 图11.1展示了随着串行执行和处理器数量变化,处理器最大限度的利用率的曲线。随着处理器数量的增加,我们很明显地看到,即使串行化执行的程度发生细微的百分比变化,都会大大限制吞吐量随计算资源增加。 第6章探究了如何识别逻辑边界,从而把应用程序分解为不同的任务。但是为了在多处理器系统中预知你的程序是否存在加速的可能性,你同样需要识别你的任务中串行的部分。
图11.1 Amdahl定律中不同串行化的百分比,带来的最大的效能
清单11.1中,假设应用程序中N个 线程正在执行doWork,从一个共享的工作队列中取出任务,并处理;假设这里的任务并不依赖其他任务的结果或边界效应。忽略任务进行队列操作的时间,如 果我们增加处理器,应用程序会随之发生什么样的改进呢?乍看这个程序可能完全由并行任务组成,并不会相互等待,那么处理器越多,更多的任务就越可能并发处 理。然而,其中也包含串行组件——从队列中获取任务。所有工作者线程都共享工作队列,因此它会需要一些同步机制,从而在并发访问中保持完整性。如果通过加 锁来守卫队列状态,那么当一个线程从队列中取出任务的时候,其他线程想要取得下一个任务就必须等待——这便是任务处理中串行的部分。
单 个任务的处理时间不仅包括执行任务Runnable的时间,也包括从共享队列中取出任务的时间。如果工作队列是LinkedBlockingQueue类 型的,这个取出的操作被阻塞的可能性小于使用同步的LinkedList的阻塞可能,这是因为LinkedBlockingQueue使用了更具伸缩性的 算法,但是访问所有共享的数据结构,本质上都会向程序引入一个串行的元素。 这 个例子同样忽略了另一个的相同的串行源(source of serialization):结果处理。所有有用的计算都产生一些结果集或者边界效应——如果不是,它们可以当作死代码(dead code)被遗弃掉。因为Runnable没有提供明确的结果处理,这些任务必须具有一些边界效应,设定把它们的结果写入日志还是存入一个数据结构。日志 文件和结果容器通常由多 示例:框架中隐藏的串行化 为 了观察并行化如何被隐藏在应用程序的架构中,我们可以比较加入线程时的吞吐量,基于观察到的可伸缩性变化来推断串行源。图11.2展示了一个简单的应用程 序中,多个线程重复从共享Queue中移出元素,并处理它们,与清单11.1类似。处理的步骤只需要线程本地的计算。如果有一个线程发现队列是空的,它会 向队列置入一批新的元素,这样其他的线程在下一次迭代中就不会无事可做。访问共享的队列显然要承担一定程度的串行化,但是处理步骤是完全并行化的,因为它 不会引用共享数据。 图11.2的曲线比较了两个均为线程安全Queue的实现:synchronizedList包装了LinkedList,另一个是ConcurrentLinkedQueue。测试跑在8-way Sparc V880,OS为
图11.2 不同队列实现之间的比较
Solaris的系统上。尽管每一次运行代表相同数量的“工作”,我们能够看到,只有改变队列的实现,才能明显影响可伸缩性。
ConcurrentLinkedQueue 的吞吐量持续改进,直到它到达了处理器数量,之后会保持不变。另一方面同步LinkedList的吞吐量,在3个线程时表现了其带来的改进,但是之后会下 跌,因为同步的开销增加了。图中当线程数为4或5时,竞争是非常激烈的,以至于每次防问队列都要竞争锁,并且吞吐量受控于上下文切换的次数。 吞 吐量的不同源自于两个队列实现的串行化不同。同步的LinkedList用一个锁守护着整个队列的状态,在offer和remove调用时都要获取这个 锁;ConcurrentLinkedQueue使用了精妙的非阻塞队列算法(参见15.4.2小节),它使用了原子引用来更新各个链接指针。这两者,其 中一个是把整个的插入或删除都实现为串行化的,而另一个则是把每个指针的更新变成串行化的。 定性地应用Amdahl定律 如果我们能精确估算出执行中串行部分所占的比重,Amdahl定律量化了使用更多资源时加速的可能性。尽管直接衡量串行化非常困难,Amdahl定律在没有这样的衡量的情况下仍然有用。 因 为我们的理想模型受我们所在环境的影响,我们中间很多人都习惯性认为多处理器系统具有2个或4个处理器,或者可能(如果我们进行大胆假设)有几打(a few dozen),因为这是近年来技术上普遍可以达到的。但是随着多核CPU成为主流,系统将具有成百上千个处理器3。看上去适合于4路系统的算法可能含有隐 藏的可伸缩性瓶颈,只不过还没有遇到而已。 当我们评估一个算法的时候,考 虑其在成百甚至上千个处理器的情况下受到的限制,能够帮助我们洞察伸缩性的极限的出现。例如,11.4.2和11.4.3两节中探讨了两种技术,用来减小 锁的粒度:分拆锁(把一个锁分拆成两个),分离锁(把一个锁分拆成多个锁)。透过Amdahl定律来审视它们,我们发现把一个锁分拆成两个,看上去没能在 利用多处理器上帮助我们很多,但是分离锁的效果却很好,因为分离出的数量可随着处理器数量的增加而增长。(当然,性能优化应该总是依据真实的性能需求;有 时候,把一个锁分拆成两个足可以满足需求了。 |