并发与并行
并发基础
并发与并行
并发就是可同时发起执行的程序,指程序的逻辑结构;并行就是可以在支持并行的硬件上执行的并发程序,指程序的运⾏状态。换句话说,并发程序代表了所有可以实现并发行为的程序,这是一个比较宽泛的概念,并行程序也只是他的一个子集。并发是并⾏的必要条件;但并发不是并⾏的充分条件。并发只是更符合现实问题本质的表达,目的是简化代码逻辑,⽽不是使程序运⾏更快。要是程序运⾏更快必是并发程序加多核并⾏。
简言之,并发是同一时间应对(dealing with)多件事情的能力;并行是同一时间动手做(doing)多件事情的能力。
并发是问题域中的概念——程序需要被设计成能够处理多个同时(或者几乎同时)发生的事件;一个并发程序含有多个逻辑上的独立执行块,它们可以独立地并行执行,也可以串行执行。而并行则是方法域中的概念——通过将问题中的多个部分并行执行,来加速解决问题。一个并行程序解决问题的速度往往比一个串行程序快得多,因为其可以同时执行整个任务的多个部分。并行程序可能有多个独立执行块,也可能仅有一个。
具体而言,早期的 Redis(6.0 版本后也引入了多线程)会是一个很好地区分并发和并行的例子,它本身是一个单线程的数据库,但是可以通过多路复用与事件循环的方式来提供并发地 IO 服务。这是因为多核并行本质上会有很大的一个同步的代价,特别是在锁或者信号量的情况下。因此,Redis 利用了单线程的事件循环来保证一系列的原子操作,从而保证了即使在高并发的情况下也能达到几乎零消耗的同步。再引用下 Rob Pike 的描述:
A single-threaded program can definitely provides concurrency at the IO level by using an IO (de)multiplexing mechanism and an event loop (which is what Redis does).
并发级别
同步、异步、阻塞、非阻塞
在并发与并行的基础概念之后,我们还需要了解同步、异步、阻塞与非阻塞这几个概念的关系与区别。
同步即执行某个操作开始后就一直等着按部就班的直到操作结束,异步即执行某个操作后立即离开,后面有响应的话再来通知执行者。从编程的角度来看,如果同步调用,则调用的结果会在本次调用后返回。如果异步调用,则调用的结果不会直接返回。会返回一个 Future 或者 Promise 对象来供调用方主动/被动的获取本次调用的结果。
而阻塞与非阻塞在并发编程中,主要是从对于临界区公共资源或者共享数据竞态访问的角度来进行区分。某个操作需要的共享资源被占用了,只能等待,称为阻塞;某个操作需要的共享资源被占用了,不等待立即返回,并携带错误信息回去,期待重试,则称为非阻塞。
值得一提的是,在并发 IO 的讨论中,我们还会出现同步非阻塞的 IO 模型,这是因为 IO 操作(read/write 系统调用)其实包含了发起 IO 请求与实际的 IO 读写这两个步骤。阻塞 IO 和非阻塞 IO 的区别在于第一步,发起 IO 请求的进程是否会被阻塞,如果阻塞直到 IO 操作完成才返回那么就是传统的阻塞 IO,如果不阻塞,那么就是非阻塞 IO。同步 IO 和异步 IO 的区别就在于第二步,实际的 IO 读写(内核态与用户态的数据拷贝)是否需要进程参与,如果需要进程参与则是同步 IO,如果不需要进程参与就是异步 IO。如果实际的 IO 读写需要请求进程参与,那么就是同步 IO;因此阻塞 IO、非阻塞 IO、IO 复用、信号驱动 IO 都是同步 IO。
锁视角的级别
在实际的部署环境下,受限于 CPU 的数量,我们不可能无限制地增加线程数量,不同场景需要的并发需求也不一样;譬如秒杀系统中我们强调高并发高吞吐,而对于一些下载服务,则更强调快响应低时延。因此根据不同的需求场景我们也可以定义不同的并发级别:
-
阻塞:阻塞是指一个线程进入临界区后,其它线程就必须在临界区外等待,待进去的线程执行完任务离开临界区后,其它线程才能再进去。
-
无饥饿:线程排队先来后到,不管优先级大小,先来先执行,就不会产生饥饿等待资源,也即公平锁;相反非公平锁则是根据优先级来执行,有可能排在前面的低优先级线程被后面的高优先级线程插队,就形成饥饿
-
无障碍:共享资源不加锁,每个线程都可以自有读写,单监测到被其他线程修改过则回滚操作,重试直到单独操作成功;风险就是如果多个线程发现彼此修改了,所有线程都需要回滚,就会导致死循环的回滚中,造成死锁
-
无锁:无锁是无障碍的加强版,无锁级别保证至少有一个线程在有限操作步骤内成功退出,不管是否修改成功,这样保证了多个线程回滚不至于导致死循环
-
无等待:无等待是无锁的升级版,并发编程的最高境界,无锁只保证有线程能成功退出,但存在低级别的线程一直处于饥饿状态,无等待则要求所有线程必须在有限步骤内完成退出,让低级别的线程有机会执行,从而保证所有线程都能运行,提高并发度。