首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

使用线程最主要的原因是提高性能1。使用线程可以使程序更加充分地发挥出闲置的处理能力,从而更好地利用资源;并能够使程序在现有任务正在运行的情况下立刻开始着手处理新的任务,从而提高系统的响应性。

这一章将探讨用于并发程序性能的分析、监测和改进的技术。不幸的是,很多改进性能的技术同样增加了复杂度,因此增加了安全和活跃度失败的可能性。更糟糕的是,有些技术的目的是改进性能,事实上产生了相反的作用,带来了其他的性能问题。尽管我们希望获得更好的性能——改进性能带来了成就感——但安全总是第一位的。首先要保证程序是正确的,然后再让它更快——而后只有当你的性能需求和评估标准需要程序运行得更快时,才去进行改进。在设计并发应用程序的时候,最大可能地改进性能,通常并不是最重要的事情。

11.1  性能的思考

改进性能意味着用更少的资源做更多的事情。“资源”的概念很广泛,对于给定的活动而言,一些特定的资源通常非常缺乏,无论是CPU周期、内存、网络带宽、I/O带宽、数据库请求、磁盘空间、以及其他一些资源。当活动的运行因某个特定资源受阻时,我们称之为受限于该资源:受限于CPU,受限于数据库。

尽管目标是希望全面提升性能,与单线程方法相比,使用多线程总会引入一些性能的开销。这些开销包括:与协调线程相关的开销(加锁、信号、内存同步),增加的上下文

切换,线程的创建和消亡,以及调度的开销。当线程被过度使用后,这些开销会超过提高后的吞吐量响应性和计算能力带来的补偿。从另一方面,一个没能经过良好并发设计的应用程序,甚至比相同功能的顺序的程序性能更差2。

为了利用并发来实现更好的性能,我们需要努力做两件事情:更有效地利用我们现有的处理资源,让我们的程序尽可能地开拓更多可用的处理资源。从性能监视器的视角来看,这意味着我们期望使CPU尽可能处于忙碌状态。(当然,这并不是让CPU周期忙于应付无用的计算;我们希望CPU做有用的事情。)如果程序是受限于计算能力的,那么我们通过增加更多的处理器就能够提高生产量;如果程序都不能保持现有的处理器处于忙碌工作的状态,添加更多的处理器也无济于事。线程通过分解应用程序,总是让空闲的处理器进行未完成的工作,从而保持所有CPU“热火朝天”地工作,

11.1.1  性能“遭遇”可伸缩性

应用程序可以从很多个角度来衡量;比如服务时间、等待时间、吞吐量、效率、可伸缩性、生产量。有一些标准(服务时间、等待时间)是用来衡量“有多快”,即给定的任务单元需要多长时间进行处理,得到回馈;另一些(生产量、吞吐量)用来衡量“有多少”,即限定计算资源的情况下,究竟能够完成多少工作。

可伸缩性指的是:当增加计算资源的时候(比如增加额外CPU数量、内存、存储器、I/O带宽),吞吐量和生产量能够相应地得以改进。

在传统的性能调优中,为配合可伸缩性来设计和调试并发应用程序是非常困难的。为并发而进行的调试,其目的通常是用最小的代价完成相同的工作,比如通过缓存来重用以前计算的结果,或者用时间复杂度为On log n)算法取代On2)算法。在为可伸缩性进行调试的时候,你的目的是如何并行化你的问题,使你能够利用额外的计算资源,用更多的资源做更多的事情。

性能的这两个方面——“有多快” 和“有多少”是完全分离的,有时候甚至是相悖的。为了实现更好的可伸缩性,或者更好地利用硬件,我们通常会停止增加每个独立任务所要完成的工作量,比如我们把任务分解到多个管道线的子任务中。据有讽刺意味的是,大多数在单线程化的程序中提高性能的窍门,都会损害可伸缩性(实例参见11.4.4)。

我们所熟知的程序的三层(tier)模型——其中的表现层、业务逻辑层和持久层是分离的,并且可能由不同的系统掌控——这个例子阐明了提高可伸缩性是如何造成性能损失的。把表现层、业务逻辑层、持久层拼装到同一个程序中,相比于在多个系统间进行了良好分解的分布式实现,前者在完成首个任务单元的性能上要高出许多。那劣势呢?即使能够实现在同一个应用程序的不同层之间传递任务,并且不存在网络的延迟,仍然要把对计算的处理清晰地分离到各个抽象层(layer)中(比如排队的开销,协调的开销,和数据拷贝),并为此付出代价。

然而,当单一的系统到达它处理的极限时,会遇到一个严重的问题:提升它的处理能力会相当困难。所以,我们通常会接受更长处理时间的性能开销,或者为每个任务单元分配更多的计算资源,从而使应用程序能够通过增加更多的资源来相应承担更大的负荷。

从性能中的多个角度来看,“有多少”方面——可伸缩性,吞吐量和生产量——在Server应用程序中往往比“有多快”受到更多的关注。(在交互式应用中,对等待时间的关注可能更加重要,这样用户就不用等待进度条的显示,也不需要知道系统究竟在做什么。)这一章主要集中在可伸缩性而不是原始的单线程化系统的性能。

11.1.2  对性能的权衡进行评估

几乎所有的工程上的决定都会遇到某些形式的折中。在建设桥跨时使用更粗的钢筋可以提高桥的负载能力和安全性,却同时会提高建造成本。尽管软件工程的决定通常不会遇到金钱和事关人类生命的风险这两者之间的抉择,但是我们经常会缺少那些能帮助我们作出正确权衡的信息。例如,“快速排序”算法对于大的数据集具有非常好的效率,但是我们熟知的“冒泡排序”对小数据集非常有效。如果你想要为实现排序例程选择一个算法,你需要知道面临处理的数据集的大小,还有你试图进行优化的目标:是平均计算时间,允许的最差时间,还是可预知性。不幸的是,库排序例程的作者所拿到的需求里面往往没有包括这些信息。这就是为什么大多数优化都不成熟的原因之一:他们通常在获得清晰的需求之前进行了假设。

避免不成熟的优化。首先使程序正确,然后再加快——如果它运行得还不够快。

当我们面临工程上的决定时,有时候会用某种形式的成本换取其他东西(用内存开销换取更短的服务时间 );有时候也会用开销换取安全性。安全性并不完全指对人们生活的威胁,比如桥梁的那个例子。很多性能的优化会损害可读性或可维护性——代码越“聪

明”,越“晦涩”,就越难理解和维护。有时候,优化需要违背好的面向对象的设计原则,比如打破封装;有时候,它们会带来很大的风险和错误,因为通常越快的算法越复杂。(如果你不能识别代价或者风险,你可能还没能对将发生的场景进行彻底、仔细地思考。)

大多数性能的决定需要多个变量,并且高度依赖于发生的环境。在决定某个方案比其他方案“更快”之前,先问你自己一些问题:

l  你所谓的更“快”指的是什么?

l  在什么样的条件下你的方案能够真正运行得更快?在轻负载还是重负载下?大数据集还是小数据集?是否支持你的测量标准的答案?

l  这些条件在你的环境中发生的频率?是否支持你的测量标准的答案?

l  这些代码在其他环境的不同条件下被用到的可能性?

l  你用什么样隐含的代价,比如增加的开发风险或维护性,换取了性能的提高?这个权衡的决定是否正确?

作出任何与性能相关的工程决定时,都应该考虑这些问题,但是这本书只关注于并发。我们为什么要推荐这样一个保守的优化方案?对性能的追求很可能是并发bug唯一最大的来源。认为同步“太慢”而导致使用看似聪明实际危险的手法,从而减少同步(比如第16.2.4小节将要讨论的双检查锁),这也成为了不遵守同步规定常用的一个借口。然而,因为并发的bug是最难追踪和消除的缺陷,所以任何引入这类bug的风险行动都需要慎重进行。

更糟的是,当你用安全性换取了性能的时候,你可能什么都没得到。特别是,当提到并发的时候,很多开发者对于产生性能问题的原因,或者哪一个方案能够更迅速,具有更好的可伸缩性,他们的直觉往往是错误的。因此,依据性能的需求(这样你能知道什么时候需要调节,什么时候该停止),根据适当的评估纲要,并使用现实中的配置和负载状况,来进行性能调节的活动是非常必要的。在调节过后,你需要再评估,以验证你已经实现了期望的改进。优化带来的安全性和可维护性风险足够严重,以至于——如果你不需要的话,你不想付出这样的代价。并且,如果你甚至没有从中得到一点好处,你绝对不希望付出此代价。

测评。不要臆测。

市场上有一些成熟的剖析工具,用来评估性能,追踪性能瓶颈,但是你不必花费大量的金钱用于了解你的程序做了什么。例如,免费的perfbar应用程序可以给你一张相当不错的图表,告诉你CPU究竟是如何忙碌地工作,并且你的目标通常是保持CPU的忙碌,这便是一个很好的方式,使你能够评估你是否需要性能调节,或者你调节的效果如何。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论