《java并发编程实战》 第十一章 如何提升性能与可伸缩性

第十一章 如何提升性能与可伸缩性

  并发程序中提升性能意味着会增加复杂性、活跃性与安全性失败的风险。

对性能的思考

  概念:资源密集型操作,例如CPU密集型、数据库密集型操作。指某个特定操作缺少CPU、数据库请求资源。
   一个并发设计很糟糕的应用程序, 其性能甚至比实现相同功能的串行程序的性能还要差。原因在于,与单线程的方法相比, 使用多个线程总会引人一些额外的性能开销。造成这些开销的操作包括:线程之间的协调(例如加锁、触发信号以及内存同步等), 增加的上下文切换, 线程的创建和销毁, 以及线程的调度等。如果过度地使用线程, 那么这些开销甚至会超过由千提高吞吐量、响应性或者计算能力所带来的性能提升。
  如何提升程序的性能,核心就是通过将 应用程序分解到多个线程上执行, 使得每个处理器都执行一些工作, 从而使所有 CPU都保持忙碌状态。(当然, 这并不意味着将CPU时钟周期浪费在一些无用的计算上, 而 是执行一些有用的工作。)
  性能可以用“多少”和“多快”来衡量,多快用服务时间、等待时间指标,多少用生产量、吞吐量来衡量计算资源一定情况下能完成多少任务。有时我们会牺牲多快来换取多少,我们通常会接受每个工作单元执行更长的时间或消耗更多的计算资源,以换取应用程序在增加更多资源的情况下处理更高的负载。
  可伸缩性指的是:当增加计算资源时(例如CPU、内存、存储容量或I/O带宽),程序的吞吐量或者处理能力能相应的增加。
   可伸缩性和性能也是相互矛盾的,提升可伸缩性往往也会造成性能损失。例如,如果把表现层、业务逻辑层和持久化层都融合到单个应用程序中,那么在处理第一个工作单元时, 其性能肯定要高于将应用程序分为多层并将不同层次分布到多个系统时的性能。 这种单一 的应用程序避免了在不同层次之间传递任务时存在的网络延迟,同时也不需要将计算过程分解到不同的抽象层次, 因此能减少许多开销
  对性能的提升可能是并发错误的最大来源。有人认为同步机制“ 太慢”, 因而采用一些看似聪明实则危险的方法来减少同步的使用, 这也通常作为不遵守同步规则的一个常见借口。然而, 由于并发错误是最难追踪和消除的错误, 因此对于任何可能会引入这类错误的措施, 都需要谨慎实施。
  对于服务器应用程序来说, “ 多少 ” 这个方面——可伸缩性、 吞吐量和生产量, 往往比 “多快 ” 这个方面更受重视。不要用安全性来换取性能。(在交互式应用程序中,延迟或许更加重要, 这样用户就不用等待进度条的指定, 并奇怪程序究竟在执行哪些操作。)本章将重点介绍可伸缩性而不是单线程程序的性能。
   免费的perfbar应用程序可以给出CPU的忙碌程度信息, 而我们通常的目标就是使CPU保持忙碌状态, 因此这个功能可以有效地评估是否需要进行性能调优或者已实现的调优效果如何。

使用Amdahl定律分析可伸缩性

大多数并发程序都与农业耕作有着许多相似之处, 它们都是由一系列的并行工作和串行工作组成的。Amdahl定律描述的是:在增加计算资源的情况下, 程序在理论上能够实现最高加速比, 这个值取决于程序中可并行组件与串行组件所占的比重。假定F是必须被串行执行的部 分, 那么根据Amdahl定律, 在包含N个处理器的机器中, 最高的加速比为:
        
                     在这里插入图片描述
  当N趋近无穷大时, 最大的加速比趋近于1/F。 因此, 如果程序有50%的计算需要串行执行, 那么最高的加速比只能是 2 (而不管有多少个线程可用);如果在程序中有10%的计算需要串行执行, 那么最高的加速比将接近10。Amdahl定律还量化了串行化的效率开销。在拥有10个处理器的系统中, 如果程序中有10%的部分需要串行执行, 那么最高的加速比为5.3 (53% 的使用率), 在拥有 100 个处理器的系统中, 加速比可以达到 9.2 (9% 的使用率)。即使拥有无限多的CPU, 加速比也不可能为10。
  如图给出了处理器利用率在不同串行比例以及处理器数量情况下的变化曲线。(利用率的定义为:加速比除以处理器的数量。)随着处理器数量的增加, 可以很明显地看到, 即使串行 部分所占的百分比很小, 也会极大地限制当增加计算资源时能够提升的吞吐率。
          在这里插入图片描述
  第6章介绍了如何识别任务的逻辑边界并将应用程序分解为多个子任务。 然而, 要预测应用程序在某个多处理器系统中将实现多大的加速比, 还需要找出任务中的串行部分。
  假设应用程序中N个线程正在执行程序消单11-1中的doWork, 这些线程从一个共享的工作队列中取出任务进行处理,而且这里的任务都不依赖于其他任务的执行结果或影响。暂时先不考虑任务是如何进入这个队列的, 如果增加处理器, 那么应用程序的性能是否会相应地发生变化?初看上去, 这个程序似乎能完全并行化:各个任务之间不会相互等待, 因此处理器越多,能够并发处理的任务也就越多。然而,在这个过程中包含了一个串行部分——从队列中获取任务。所有工作者线程都共享同一个工作队列, 因此在对该队列进行并发访问时需要采用某种同步机制来维持队列的完整性。如果通过加锁来保护队列的状态, 那么当一个线程从队列中取出任务时, 其他需要获取下一个任务的线程就必须等待, 这就是任务处理过程中的串行部分

      
    在这里插入图片描述
  单个任务的处理时间不仅包括执行任务Runnable的时间,也包括从共享队列中取出任务的时间。如果使用LinkedBlockingQueue作为工作队列,那么出列操作被阻塞的可能性将小于使用同步LinkedList时发生阻塞的可能性,因为LinkedBlockingQueue使用了一种可伸缩性更高的算法。然而,无论访问何种共享数据结构,基本上都会在程序中引人一个串行部分
  这个示例还忽略了另一种常见的串行操作:对结果进行处理。所有有用的计算都会生成某种结果或者产生某种效应一如果不会,那么可以将它们作为“ 死亡代码” 删除掉。由于Runnable没有提供明确的结果处理过程,因此这些任务一定会产生某种效果,例如将它们的结果写人到日志或者保存到某个数据结构。通常,日志文件和结果容器都会由多个工仵芍旨线程共享,并且这也是一个串行部分。如果所有线程都将各自的计算结果保存到自行维扩喽妇居结构中,并且在所有任务都执行完成后再合并所有的结果,那么这种合并操作也是一个串行部分。
  在所有并发程序中都包含一些串行部分。如果你认为在你程序中不存在串行部分,那么可以再仔细检查一遍。
示例:在各种框架中隐裁的串行部分
  要想知道串行部分是如何隐藏在应用程序的架构中,可以比较当增加线程时吞吐量的变化,并根据观察到的可伸缩性变化来推断串行部分中的差异。图11-2给出了一个简单的应用程序,其中多个线程反复地从一个共享Queue中取出元素进行处理,这与程序清单11-1很相似。处理步骤只需执行线程本地的计算。如果某个线程发现队列为空,那么它将把一组新元素放人队列,因而其他线程在下一次访间时不会没有元素可供处理。在访问共享队列的过程中显然存在着一定程度的串行操作,但处理步骤完全可以并行执行,因为它不会访问共享数据。
    在这里插入图片描述
  图11-2的曲线对两个线程安全的Queue的吞吐率进行了比较:其中一个是采用synchronizedList封装的LinkedList; 另一个是ConcurrentLinkedQueue。这些测试在8路Spare V880系统上运行,操作系统为 Solaris。尽管每次运行都表示相同的 “ 工作量”,但我们可以看到, 只需改变队列的实现方式, 就能对可伸缩性产生明显的影响。
  ConcurrentLinkedQueue 的吞吐量不断提升,直到到达了处理器数量上限, 之后将基本保持不变。 另一方面, 当线程数量小于 3 时,同步 LinkedList 的吞吐量也会有某种程度的提升,但是之后会由于同步开销的增加而下跌。 当线程数最达到 4 个或 5 个时,竞争将非常激烈,至每次访问队列都会在锁上发生竞争,此时的吞吐量主要受到上下文切换的限制。
  吞吐量的差异来源于两个队列中不同比例的串行部分。 同步的 LinkedList 采用单个锁来保护整个队列的状态, 井且在 offer 和 remove 等方法的调用期间都将持有这个锁。 ConcurrentLinkedQueue 使用了一种更复杂的非阻塞队列算法(请参见 15.4.2 节),该算法使用原子引用来更新各个链接指针。 在第一个队列中,整个的插入或删除操作都将串行执行, 而在 第二个队列中, 只有对指针的更新操作需要串行执行。

Amdahl定律的应用

  书中原文,如果能准确估计出执行过程中串行部分所占的比例,那么 Amdahl 定律就能量化当有更多计算资

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值