乐趣区

当我们在说“并发、多线程”,说的是什么?

这篇文章的目的并不是想教你如何造火箭(面试造火箭,工作拧螺丝),而是想通过对原理和应用案例的有限度剖析来协助你构建起并发的思维,并将操作系统的理论知识与工程实践结合起来,贯穿从学到会的全过程。当然,虽然我们是从实用角度出发,但具有实践意义的深层次知识点永远会是面试中的杀手锏,这可比只能口头造火箭的理论知识更吸引面试官。
本文适合谁:

希望能了解并发概念的初学者
需要理清并发概念与技术的工程师
对并发在工作中的应用与其底层实现原理感兴趣的读者

在这篇文章中,你将了解到并发与多线程相关的一系列概念,通过一些例子我们可以在不纠结于具体的技术细节的情况下形成对并发与多线程相关的各种概念的抽象理解。有了这些概念以后,我们再去学习具体的理论和技术细节就是手到擒来的事了。
什么是并发?
最近几年淘宝发展得如火如荼,涌现出了一大批白手起家的卖家。想象一下你是一个刚刚起步的小卖家,自己运营一个服装网店,每天都要自己打包发货。刚开始时生意一般,每天自己一个人一个小时就能干完。
随着生意的蓬勃发展,发货时间慢慢地从一个小时涨到了两个小时、四个小时,一次因为延迟发货导致被投诉之后,你终于觉得该招更多的人了。很快,两个小伙伴加入了你的事业,打包速度开始有了质的提高。这就是最基本的并发了,每个人都可以看成是一个线程,同样的工作量,干得人多了自然就快了。
所以并发就是通过多个执行器同时执行一个大任务来缩短执行时间、提高执行效率的方法。
数据竞争
但是好景不长,周末一盘货,你发现少了不少。这办公室里也没遭贼,怎么就会少货呢?细细一查快递单,你发现竟然有几单发重了。之后的几天你都细细留意了一下发货的过程,最后发现是因为每个人都会拿着一张发货清单去备货,如果有一些订单不小心打印重复了,就有可能会被不同的人重复发货。虽然数量不多,但是也很心痛啊。这个问题产生的原因就是因为每个人在备货之前拿到的订单状态(未发货)在实际备货时发生了变化(已由其他人发货)。这种对一个共享数据(订单的发货状态)本应独占的读取、检查、修改过程,如果发生了并发,这种情况就被称为数据竞争。而这个读取、检查、修改的过程就被称为临界区,临界区指的就是一个存在数据竞争的代码片段。
数据竞争出现的根本原因是一个数据本来应该只能由一个执行器完整地执行读取、检查、修改过程,但是如果出现了并发,那么就没办法保证到了“修改”这一步时的数据还保持了“读取”时的值了。
确定原因后,有人想到了一个好办法,可以打印一张总的发货清单,这样所有人都必须以这个清单上的订单是否发货来确定是否要对订单进行备货并发货了。因为清单只有一份,所以每次只能由一个人来修改订单的发货状态。这种只能由一个执行器进行数据修改操作来避免发生数据竞争问题的做法就被称为互斥,也就是我们常说的锁了。
分布式并发概念
分布式
因为你管理得当,生意发展得很快,现在的办公室里已经堆不下所有衣服了。所以你又租了一个仓库来同样进行发货。两个地方都会进行发货,那么就可以把每一个仓库理解为一台独立的计算机,这样通过多台计算机完成同一任务的方式就可以被称为分布式,这样的一组计算机的集合就被称为集群。
这时候之前用一张纸质的总发货清单的数据竞争解决方式就行不通了,所以我们需要把这张总发货清单放到云端,让大家可以通过网络进行编辑,但是每次只能一个人编辑。在这种情况下,我们可以把两个仓库各自看成一台计算机 / 进程,而每个仓库里的人就是这个进程中的线程。这样的话这张总发货清单就成为了一个分布式锁,因为它每次只能有一个人编辑,所以是一个互斥锁,或者简称为锁;而因为它可以被两个进程 / 计算机(仓库)共同使用,所以被称为是分布式锁。
什么是进程 / 线程?
可以简单地将进程理解为我们电脑 / 手机上的一个应用,同一台手机上的每个 App 都是一个进程,同一个 App 在每个手机上也是一个进程。进程和进程之间可以理解为是两个仓库,互相之间物理隔离;而线程就是仓库里的每一个人,他们共享同一个办公空间。这里的办公空间就可以理解为操作系统中的虚拟内存空间,但是本文主要讨论并发相关的概念,就不继续展开了。
分布式数据不一致
因为生意比较好,所以所有人都很忙。有时候就会因为有一些人虽然在云端表格上已经勾上了一个订单,但是一忙就给忙忘了。其他人怕重复发货又不会再去处理已经勾上的订单了,因为这样导致的未发货订单让店铺被投诉了好多次,影响非常大。这种在并发过程中修改了数据状态但是没有完成后续执行的情况就会出现数据不一致,即订单已经被勾上,但实际并没有发货。
但是作为聪明的老板,你又想到了解决的方法。每隔一小时两个仓库就会各派一个人检查一下已经勾上的订单是否已经都打包完贴上快递单了。这种每隔一段时间就检查并处理遗漏的数据不一致订单的任务就被称为兜底任务。而通过兜底任务实现的在最后所有订单都会达到数据一致状态的情况就被称为最终一致性。
优化方式
大家可能早就觉得前面介绍的总发货清单的方法太傻了,只要每个订单都只打印一张发货清单,由单独一个人去负责分发清单就可以了,其他人只要处理好自己被分配到的订单就可以了。最后再加上一个兜底任务对订单的发货情况进行二次校验基本上就不会发生漏发或者重发的情况了。这种由一个执行器进行任务拆分,再由一组执行器进行处理,最后再由一个或一组执行器进行结果汇总的处理方式就是现在非常流行的 map-reduce 方法了。这种方法在大数据或者程序语言标准库里都有大量的应用,比如大数据领域赫赫有名的 Hadoop 和 Java 语言中的 ForkJoinPool 都使用了这种思想。
回顾
在这篇文章中,我们涉及到了以下的技术名词:

并发,通过多个执行器同时执行一个大任务来缩短执行时间、提高执行效率的方法。
数据竞争,对一个共享数据本应独占的读取、检查、修改过程发生了并发的情况。
临界区,存在数据竞争的代码片段。
互斥锁(也可以简称为“锁”),同一时间只能由一个执行器获取的实体,用于实现对临界区的互斥(只有一个)访问。
分布式,通过多台计算机完成同一任务的方式。
集群,一组完成同一任务的机器。
分布式锁,在不同机器 / 进程上提供互斥能力的锁。
数据不一致,一系列操作不具有原子性,一部分执行成功而另一部分没有,导致不同数据之间存在矛盾,例如订单已经是发货状态,但是实际没有发货。
兜底任务,处理数据不一致状态的任务。
最终一致性,通过兜底任务或其他方式保证数据不一致的情况最终会消失。
map-reduce,一种任务拆分 - 执行 - 再合并的任务执行方式,可以有效地利用多台机器、多核 CPU 的性能。

后记
因为并发的知识范围很大,而且对于一些抽象概念的传递必然会需要花费一些篇幅,所以这个主题将会包含一系列文章,主要覆盖以下主题:

什么是并发?
抛开冗长繁杂的技术点,直接理解并发相关的各种概念。

什么是多线程?
多线程是并发的一种重要形式。通过具体的多线程问题引出多线程编程中的关键点和对应的工具与知识点,轻松学会多线程编程。

常用工具中的并发实现
通过解析知名开源工具中的并发方案实现来深入理解并发编程实践。

有兴趣的读者可以继续关注后续的文章,在之后的文章中会有对并发编程、操作系统原语、硬件原语等等理论与实践知识的详细介绍与案例。
对数据库索引感兴趣的读者可以了解一下我之前的文章:

数据库索引是什么?新华字典来帮你 —— 理解

数据库索引融会贯通 —— 深入

20 分钟数据库索引设计实战 —— 实战

数据库索引为什么用 B + 树实现?—— 扩展

退出移动版