并发与并行
很多初级程序员都会把并发和并行搞混在一起,或者认为并发和并行根本就是一回事。而其实并发和并行压根就不是在讨论一个问题。本文我们就一起来看看究竟什么是并发和并行,以及如何实现并发和并行。
并发与并行
并发(concurrency)用于描述问题。如果一个问题可以被拆分成多个问题进行局部求解,或者更本质的说,如果每个局部的子问题的求解都不因其求解的顺序改变而改变,那么这就是一个可并发的问题。(再次注意:并发描述的是问题)
并行描述的是解决问题的方法。二十个人一起搬砖也好,二十个 CPU 核一起计算也好,并行描述的就是将任务分配给多个可以执行任务的执行者,分别执行任务的方式。
举个简单的例子。大家都应该去过图书馆借书,假设现在图书馆有一个前台管理员,A 同学去前台借书,同时 B 同学去前台还书,C 同学在咨询某个问题。这时候,图书管理员可以同时处理你们的”请求”,先从 A 手中接过书,然后回到 C 同学的问题,再用扫码枪扫 B 同学的书,然后帮 A 同学办理借书登记。整个过程中,管理员执行的任何一个子任务都不会因为其处理的先后顺序而导致其处理结果的变化。因此我们说,图书管理员的工作是一个可并发的工作(可以”同时”处理多个任务)。事实上,类比到服务端开发上,图书管理员不过就是一个开放了若干接口(借书,还书,咨询等)的服务器么。
而随着来图书馆的同学越来越多,尽管图书管理员依旧面对一个可并发的问题,但是其处理问题的速度(计算能力)是有限的,而不停在不同任务之间切换(上下文切换)更增加了他处理问题的时间成本。于是图书馆决定增加两名前台管理员,这就是并行处理问题的方式。
后端服务通常都是在处理可并发的问题。在不增加计算能力的情况下,通过增加线程(这里的线程是一个抽象的概念,可以理解为一组具有独立上下文的计算资源)的方式可以让服务能够更好的处理并发问题(同时处理多个任务)。当某个任务需要占用较长时间,但并不占用 cpu 时,服务可以切换去其他任务,让 cpu 先去处理其他任务,之后再回来。
矩阵乘法
我们再换个例子,google 引以为傲的 page rank 算法中最关键的一步就是计算两个矩阵的乘积,然后得到不同网页之间的相关性。但是现在的互联网太大了,网页数都是以亿为单位的。计算两个超大矩阵的乘积显然是一个非常耗时的工作。如果用一台计算机算显然是不可能的。
事实上这压根不是一个计算机问题,而是一个矩阵乘积计算的数学问题。简单来说,两个矩阵的乘积可以由其各自的部分子矩阵的乘积组合而乘(非常基础的线性代数知识)。假设我们要计算两个 300,000,000 _ 300,000,000 的矩阵的乘积,我们可以将其分别划分为 9 个 100,000,000 _ 100,000,000 的矩阵,一共需要进行 27 次 100,000,000 * 100,000,000 的矩阵的乘法和若干次矩阵加法(相比于矩阵乘法而言,矩阵加法的时间复杂度要低很多,可以暂且忽略)。
现在我们的超大矩阵乘法就变成了 27 个稍微小一点的矩阵的乘法了。尽管看上去没什么了不起,但是这个特性却是决定性的,因为我们可以把矩阵乘法拆分到不同的机器上计算,再集合到一起就行了。事实上,你可以继续拆分,把 100,000,000 _ 100,000,000 的矩阵继续拆分成 100 个 10,000,000 _ 10,000,000 个矩阵,这时候问题就变成了 27000 个 10,000,000 * 10,000,000 的矩阵的乘积。
如果你有 27000 台服务器的话,这个超大矩阵计算的时间复杂度就降低了 27000 倍!!原本需要计算一个月的问题现在只需要一分半钟就可以解决。
感谢上帝,矩阵乘积是一个可并发问题!!
一些计算模型
线程与锁:通过开多个线程,我们就可以得到高并发的系统。但是现实往往没有那么简单。在很多时候,多个线程往往会共享同一份数据,这个时候竞态就出现了。为了解决竞态问题,我们可以用锁机制来保护数据的读写。
函数式编程:不共享可变数据,因此没有副作用,所有的中间状态都是确定的而不是需要通过锁来防止计算过程被影响。由于这些特性,函数式编程天然具备高并发特性,你可以将计算过程分配到不同的地方进行计算,而结果不会有任何改变。这一切看起来是如此完美,而现实往往没有那么简单,现实中的问题也没有这么完美。比如在函数式编程语言 haskell 中,从类型签名可以看出一个函数是否带有副作用(不纯函数的类型签名都以 IO 开头)。
CSP (Communicating Sequential Processes):工作者之间可以通过 channel 来相互通信,当一个工作者向 channel 发送信息的时候,默认会阻塞,直到接受者把这条消息拿走。同样接收者也会默认阻塞在拿消息的地方,直到有人向 channel 发消息并被拿到。CSP 的代表语言是 golang(但是 golang 中的 channel 可以有 buffer)
ACTOR 模型:每个工作者都会有一个收件箱,当工作者需要相互通信的时候可以通过向彼此邮箱发消息来实现。和 CSP 比较来说,可以认为 actor 是实名邮箱,而 csp 是匿名邮箱。actor 模型的代表语言是 erlang。