Amdahl定律和可伸缩性
性能的思考
提升性能意味着可以用更少的资源做更多的事情。但是提升性能会带来额外的复杂度,这会增加线程的安全性和活跃性上的风险。
我们渴望提升性能,但是还是要以安全为首要的。首先要保证程序能够安全正常的运行,然后在需要的时候进行性能优化,并且优化后的程序要尽可能保持并发性,让多处理中每个cpu尽可能得不要空闲,但是如果程序的并发没有设计好,那么可能会出现并发程序没有利用好现代多处理器的优势而导致并发化后的性能还不如串行化性能。
相较于单线程,多线程有很多独有的性能开销因素:
(1)线程之间的协调(如,加锁、内存同步等)
(2)增加的上下文切换
(3)线程的创建和销毁
(4)线程调度
可伸缩性:当增加计算机资源时(资源指:CPU、内存、硬盘),程序的吞吐量会得到提升。
Amdahl定律
在增加计算资源的情况下,程序在理论上能够实现最高的加速比,这个值取决于程序中可并行组件与串行组件所占的比重。
假定F是必须被串行执行的部分,那么根据Amdahl定律,在包含N个处理器的机器中,最高的加速比为:
Speedup<=1/(F+(1−F)/n)
当N趋近于无穷大时,最大的加速比趋近于1/F。因此有50%的计算需要串行执行,那么最高的加速比只能是2。
利用率:加速比除以处理器的数量。
随着处理器数量的增加,可以很明显地看到,即使串行部分所占的百分比很小,也会极大地限制当增加计算资源时能够提升的吞吐率。
在所有的并发程序中都包含一些串行部分(比如工作线程从队列拿取任务的时候,就需要加锁(串行化))。
Amdahl定律告诉我们:在串行化程序占比固定下,加速器越多,加速比也会越高,程序的可伸缩性和处理能力也就越好,但是也需要适可而止,因为当任务数比较少,但是处理器又很多,此时处理器的利用率也会降低(当然如果你有钱,觉得处理器想买多少就买多少当我没说)。
在各个框架中隐含的串行部分
上面说到,不管什么样的并发程序中,都必定有串行部分,那么以任务队列为例,比如工作线程从队列拿取任务的时候,就需要加锁(串行化)
图中对两种线程安全的Queue进行了比较,一个是同步容器SynchronizedLinkedList,一个是并发容器ConcurrentLinkedQueue,这俩有啥区别呢?图中显示,当线程数越多时,并发容器ConcurrentLinkedQueue的吞吐率的增幅相当快,而同步容器SynchronizedLinkedList几乎没有任何变化!why?
(1)同步容器的同步方式几乎都是在一个方法上加锁(锁住整个方法),锁住整个方法会带来的问题:
(a)独占锁会降低代码的可伸缩性:因为大量的线程都会因为锁而阻塞,那么程序的串行化占比会上升,所以根据Amdahl定律,加速比就会降低,可伸缩 性降低。
(b)锁的请求频率:对一个锁请求的频率越高,就说明发生竞争的可能性越大,会限制可伸缩性(可以采取分段锁解决)。
(c)锁的请求时间:锁住整个方法比锁住某个代码块的持锁时间更长,锁持有时间过长会让程序串行化,限制可伸缩性。
(2)并发容器在安全处理上更为细粒度,因为采用的非阻塞式的算法:
(a)基于CAS(底层硬件提供并发机制)的并发程序,可伸缩性更好。
(b)摒弃了基于锁的机制,所以每个线程进来后都不用在锁上产生竞争而阻塞,并且不会产生线程的上下文的切换。
减少上下文切换的开销
任务在运行和阻塞这两个状态之间转换时,就相当于一次上下文切换。
多个线程同时记录日志:在输出流的锁上发生竞争。
I/O操作阻塞:操作系统将这个被阻塞的线程从调度队列中移走并直到I/O操作结束。
请求服务的时间不应该过长:服务时间越长,意味着越多的锁竞争。
通过将I/O操作从处理请求的线程中分离出来,可以缩短处理请求的平均服务时间。调用LOG方法的线程将不会再因为等待输出流的锁或者I.O完成而被阻塞,它们只需要要将消息放入队列,虽然在消息队列上可能会发生竞争,但put操作相对于记录日志的I/O操作(可能需要调用系统调用),是一种更为轻量级的操作,因此在实际上阻塞的概率更小,只要队列没填满。由于发出日志请求的线程被阻塞的概率降低,因此该线程在处理请求时被竞争的出去的概率也会降低。
通过将I/O操作移动了另一个用户感知不到开销的线程上,通过把所有记录日志的I/O转移到一个线程,还消除了输出流上的竞争,因此又去掉一个竞争来源。这将提升整体的吞吐量。