为什么需要并行
你真的需要并行吗
我需要并… 不你不需要.jpg
Linux之父Linus Torvalds说过一句话:
Give it up. The whole “parallel computing is the future” is a bunch of crock.
摩尔定律逐渐失效,必须从软件设计上提高并行计算能力,而且互联网、云计算、大数据成为主流,不再是一个用户在一个机器上完成所有计算的生命周期,很多场景是不同用户在同一个机器上计算,如何快速并且互不干扰保证结果正确,是并发需要解决的问题。
多核
多个CPU,CPU多核的出现让并行计算让摩尔定律的实效并不致命,也就意味着可以通过资源堆叠提供硬件基础,更大规模的计算可以在多核和集群的环境下实现并发,研究并发还是挺必要的。
噩梦
并发是程序员的噩梦,简单的功能可能因为并发的引入而变得特别复杂,维护也比较困难,线程安全也是一个必须考虑的问题,既要实现并发又要考虑线程安全那就是一个更复杂的学问了。
一些定义
- 同步(
Synchronous
)和异步(Asynchronous
):同步是阻塞的,必须等到返回结果后才能继续执行后续操作;异步是非阻塞的,无需等到返回结果就可以执行后续操作,异步方法会在别的线程执行,执行完成后再通知调用者。 - 并发(
Concurrency
)和并行(Parallelism
):前者是多个任务同时提交,强调无序性;后者是同一个时间点上多个任务一起执行,强调同时性; - 临界区:临界区用来表示一种公共资源或共享数据,比如文件、IO等,临界区资源一旦被某个线程占用,其它线程必须等到该线程释放资源后,才能访问该资源;
- 阻塞(
Blocking
)和非阻塞(Non-Blocking
):阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。和同步异步的主要区别在于,同步异步更加关注获取结果的方式,是被动地获取结果;阻塞与非阻塞更关注过程,阻塞时,如果调用方法未返回结果,就把自己挂起,等到方法返回后再激活,非阻塞时,先干别的事,过一会儿再来查看有没有返回结果,是主动获取结果。 - 死锁(
Deadlock
)、饥饿(Starvation
)和活锁(Livelock
):都是多线程的活跃性问题,死锁是线程间相互占用对方需要的资源,一直在等待对方释放资源,导致所有线程无法再继续执行下去;饥饿是一个或多个线程一直得不到所需的资源,导致这些县城一直无法执行;活锁和死锁相反,两个线程相互占用对方的资源,但是两个线程都秉承“谦让”的原则,把资源主动释放给对方,就会出现资源永远不能同时被一个线程占用,线程也无法执行下去; - 并发级别:
- 阻塞:一个线程得到临界区资源(
synchronized
或重入锁),别的线程必须等到该线程释放临界区资源后才能执行; - 无饥饿:导致饥饿的原因一般都是因为优先级的问题,如果一个低优先级的线程在得到资源之前被一批高优先级的线程插队,而且这些高优先级的线程源源不断,那么这个低优先级的线程似乎永远都得不到执行。解决这个问题只需要锁是公平的,不论优先级高低,想要得到资源必须遵守先来后到,乖乖排队即可;
- 无障碍:前两个级别都是一个一个的进入临界区,无障碍即是多个线程可以同时进入临界区,大家一起修改共享数据,但是一旦发现别人已经修改过,马上回滚自己的操作,再次尝试修改,直到只有自己在完成修改。但是当临界区冲突严重时,几乎所有线程都在回滚,没有一个线程正在走出临界区,所以共享数据一般有一个一致性标记,各个线程都可以读取这个标记,但是修改标记的操作是原子操作,同一时间只可能有一个线程在修改标记,当线程准备修改共享数据时,检查自己读取的标记和共享数据的标记是否一致,如果一致则更改,否则重头开始,这其实就是一种乐观锁策略,适用于并发不高的情况下。
- 无锁:
CAS(Compare and Swap)
就是一种无锁模式,在准备更改共享数据A的时候,先从主内存中拷贝一份共享数据的值A’,自己准备修改共享数据为B,那么先比较A与A’,如果相等,说明未被其他线程修改,可以执行修改,如果不等,那么重新拷贝A值到A’,再进项CAS,直到成功,其中CAS操作是原子的。 - 无等待:无锁只要求某一个线程在有限步骤内完成,而无等待要求所有线程都在有限步骤内完成。
RCU(Read-Copy-Update)
就是一种典型的无等待结构,对数据的读取没有任何限制,任何线程都能在第一时间读取共享数据的值,在修改数据时,先拷贝一个原始数据的副本,然后修改这个副本,再在合适的时机写回原数据,这个时机就是所有引用该数据的线程都退出对共享数据的操作。
- 阻塞:一个线程得到临界区资源(
并行相关的定律
加速比: 加速比 = 优化前的系统耗时 / 优化后系统耗时
Amdahl定律
Amdahl
定律定义了串行系统并行化后加速比的计算公式和理论上限,设
T
1
T_1
T1表示只有一个CPU的情况下系统的耗时,
T
n
T_n
Tn表示使用
n
n
n个处理器优化后的耗时,
F
F
F表示优化后代码中只能串行执行的比例,则:
T n = T 1 ( F + 1 n ( 1 − F ) ) T_n = T_1(F + \frac{1}{n}(1 - F)) Tn=T1(F+n1(1−F))
设系统加速比为 S S S,则
S = T 1 T n S = \frac{T_1}{T_n} S=TnT1
由上面两个等式可得:
S = T 1 T n = T 1 T 1 ( F + 1 n ( 1 − F ) ) = 1 F + 1 n ( 1 − F ) S = \frac{T_1}{T_n} = \frac{T_1}{T_1(F + \frac{1}{n}(1 - F))} = \frac{1}{F+\frac{1}{n}(1 - F)} S=TnT1=T1(F+n1(1−F))T1=F+n1(1−F)1
在CPU核数一定的情况下,系统加速比在 1 − n 1-n 1−n之间, F F F占比越低,系统加速比越高;在 F F F占比一定的情况下,系统加速比在 1 − 1 F 1 - \frac1F 1−F1之间,CPU核数越多,系统加速比越高。所以想要通过并行加速系统,必须同时考虑到增加CPU核数和串行代码并行化,否则可能并没有多大效果。
Gustafson定律
Gustafson
定律也试图解释加速比和串行占比、处理器个数之间的关系,不过切入点不同。Amdahl
定律的隐藏前提是:程序的计算量是一定的,执行完了就没有了。假设有一个实时计算系统,计算一直在进行,似乎没有停下来的迹象,没法定量分析,那怎么分析呢?这就可以用时间作为定量来分析了。
在同一段时间
t
t
t内,串行计算量是
a
a
a,可并行部分的计算量是
b
b
b,那么如果使用串行,t
时间内总的计算能力为
a
+
b
a + b
a+b,如果使用并行,计算能力为
a
+
n
b
a + nb
a+nb,其中
n
n
n为处理器个数,加速比为:
S = a + n b a + b S = \frac{a + nb}{a + b} S=a+ba+nb
串行占比为:
F = a a + b F = \frac{a}{a + b} F=a+ba
有上面两个等式可得:
S = a + n b a + b = a a + b + n b a + b = F + n ( a + b − a a + b ) = F + n ( 1 − F ) = F + n − n F = n − F ( n − 1 ) S = \frac{a + nb}{a + b} = \frac{a}{a+b} + \frac{nb}{a + b} = F + n(\frac{a+b-a}{a+b}) =F +n(1-F)=F+n-nF=n-F(n-1) S=a+ba+nb=a+ba+a+bnb=F+n(a+ba+b−a)=F+n(1−F)=F+n−nF=n−F(n−1)
在
F
F
F或
n
n
n一定的情况下可以得出和Amdahl
定律一样的趋势以及上限,但是这两个定律的切入点不同:Amdahl
定律更加适合证明串行比例一定的情况下,无论增加多少CPU都是徒劳的,串行比例决定了加速比的上线;而Gustafson
更加适合证明,在串行占比很小的情况下,那么加速比就可以随着CPU个数线性增长。
由此得出结论:想让程序运行的更快,在并行占比较大的系统中,增加CPU个数是比较好的方法;在并行占比较小的系统中,增加并行占比是当务之急。