在这里你可以了解:
- 为啥大家说的进程的意思有出入?
- 为啥并发那么难理解?
- 为啥高并发不仅仅是“高”+“并发”的意思?
-
进程,和另一种进程
假如你想铺一条长1000m,宽50m的路。为了解决这个问题,你先构想出来假如你自己1个人做,整个过程第一步干什么,第二步干什么等等。这个干活的过程,可以被称作一个【**进程**】(Process),或者你可以理解为“一个做事的办法/步骤/方案“。进程的英文Process本意就是“过程”的意思,是一个抽象的概念。这个活有没有真得干并不重要,重要的是你已经预先想好了这个活该怎么干,有了一个可行思路。
你把这套铺路的方案用纸张写出来就得到一个【**程序**】。在软件开发中也是如此,只不过你用的不是纸笔,而是某种编程语言。注意,这里【进程】仅仅是描述这个方案的。至于这个方案是在脑海里,还是已经被执行了,是不重要的。
当然,大家更加熟知的进程往往指的是另外一个意思,是指“程序在操作系统中运行的实例“。所谓“实例”是指同一个程序可以同时在操作系统里实际的运行。就像是如果你的铺路程序写好了,可以铺好几条路。每一个具体的铺路工作是一个“实例”。
所以wiki是这么给定义的: In computing, a > process is the > instance of a > computer program that is being executed by one or many threads. 见 https://en.wikipedia.org/wiki/Process_(computing)
为了避免混淆,我在下文中将操作系统的这个进程概念称为【OS进程】。而对上一节里面讲的“想办法”的进程称为【P进程】。
【OS进程】到底怎么实现呢?铺路的工作真的开干时,要不断记录买了什么料,已经花了多少钱,哪一块已经铺好了,哪一块刚铺完沥青得晾着等等。这些信息只有工作真的开干才会有。【OS进程】也是一样,因此比如Linux将进程实现为“task_struct”,里面记录了CPU要完成这个工作的一整套数据。比如一个事情A,CPU没做完,被程序员要求做另外一件事情B。就得找个地方记录做了一半的A的那些数据,以便于CPU回过头来再做A时能够继续。
再次强调下,【P进程】和【OS进程】并不是一个意思,尽管会有一些关联。所以在阅读各种资料时一定要根据上下文分清楚进程到底是什么意思。我再总结下: 【P进程】指的是如何想明白做一件事情的过程。他用来帮助你理清做事的思路。这个事情做与没做,对于【P进程】这个概念不重要。
【OS进程】是指程序真的运行起来的实例,可以被实现为存放调度给CPU的任务和状态的数据结构。 软件设计里有一个经典的4 + 1 View,其中一个View叫做“Process View”,里面的Process就是指这里的【P进程】。“Process View”的目标就是“把怎么解决问题的方案说明白”。
线程
上面wiki的定义指出一个【OS进程】是由一到多个【线程】组成。这里的【线程】(Thread)是一个抽象概念。
但在Linux中,【线程】是被实现为“轻量级进程”的。也就是说在Linux中的进程和线程实现的本质是一样的。只不过在以下2点上有显著区别:在资源消耗上进程的消耗多,线程消耗相对少,以及;
- 内存空间上有一些不同:进程的虚拟内存彼此隔离,而线程则共享同一虚拟内存空间有些不同。
但Linux中【OS进程】和【线程】都用作任务调度单位。因此,Linux这种实现方式和理论上的概念不是很吻合,但是大量的程序已经跑在这个模式上了。而且大家早就已经习惯了。其他操作系统对【OS进程】和【线程】的实现会有所不同。如果碰到了不要惊讶。
并发
【并发】(Concurrency)是由【P进程】引申出来的抽象概念。
上面说到了你可以假设自己一个人按照一定的步骤来铺路,一个人从头干到尾,这是一个“串行”的【P进程】。
但你也可以假设有2个人铺路。比如你可以按照长度分两半,一人铺500m 50m;也可以按宽度划分,一人铺1000m 25m;你还可以说让一个人负责铺全部路面的前5个步骤,另外一个人负责铺路面的余下5个步骤。然后你可以进一步想,假如不是雇2个人,而是雇20个人概如何分工呢?你可以混搭按长度,宽度,步骤等各种方式进行拆分。你甚至可以考虑这20个人不是完全一样的,有的能力强,有的能力弱,可以适当的调整工作量的比例等等。
不管怎样拆,都意味着你得到了【并发】的【P进程】。换成说人话就是,你有一套方案,可以让多个人一起把事情做的更高效。注意是“可以“让事情更高效,而不是“必然“让事情更高效。是不是更高效要看到底是怎么执行的,后边会讲。
举个写代码的例子,你有一个很长很长的数组,目标是把每一个数都2。一个并发的做法就是把数组拆为很多个小段,然后每个小段的元素依次自己2。这样的程序写出来就是一个【并发】的【程序】。这个程序如果运行起来就是【并发】的【OS进程】。
这时就会出现一个问题,当你想把一个【并发】的【P进程】写成程序时,你怎么用编程语言告诉操作系统你的程序的一些步骤是【并发】的。更确切地说,你需要一个写法(可能是语法,也可能是函数库)表达:
- 几个任务是【并发】的
- 【并发】的任务之间是怎么交互协作的
为了解决这两个问题,人们总结了一些方法,并将其称为“并发模型”。
比如:
- Fork & Join模型(大任务拆解为小任务并发的跑,结果再拼起来)
- Actor模型(干活的步骤之间直接发消息)
- CSP模型(干活的步骤之间订阅通话的频道来协作)
- 线程&锁模型(干活的人共享一个小本本,用来协作。注意小本本不能改乱套了,所以得加锁)
- ……
以Java中的线程为例,大家想表达【并发】就启动新的Thread(或者某种等价操作,如利用线程池);想让Thread之间交互,就要依靠共享内容。但是【并发】的Thread如果同时修改同一份数据就有可能出错(被称为竞争问题),为了解决这个问题就要引入锁(Lock,或者一些高级的同步工具,如CountdownLatch,Semaphore)。
特别强调下,Java的线程是表达并发的概念的类。这个类在绝大部分操作系统上使用操作系统内核中的【线程】实现。二者之间还是有一些细微的差异。即用开发者用Java Thread写代码表达思路,和操作系统调度线程执行是两个层面的事情。请努力认识到这一点。
再比如Erlang是基于Actor的并发模型(其实这是原教旨主义的OO)。那么就是每个参与【并发】的任务称为Process(又一个进程……,和【P进程】以及【OS进程都不太一样】,叫【E进程】好了,Erlang中的”进程“)。【E进程】之间通过消息来协作。每个【E进程】要不是在处理消息,要不就是在等新的消息。
如果你用go,那么表达并发的工具就是goroutine,goroutine之间协作要用channel。(当然也可以用Sync包加锁,不展开)。
对于并发模型《7周7并发模型》这本书讲的非常好。推荐阅读。书中展示了七种最经典的并发模型和大量的编码实例。
并行
现在我们已经有了一个【并发】的想法,然后进入执行层面。
回到上面铺路的例子,你虽然假设有20个人可以一起干活。但你不一定真的能雇得到20个人。假如说你实际上最终只雇到1个人。但你有一个为20个人一起干活设计的方法。能不能用呢?当然能,只要让这个人先干第1人份的活,再干第2人份的……
但如果你真的雇了10个人,就可以很容易的让第1个人干第1人份和第2人份的活,第2个人干第3和第4人份的活…… 而这10个人同时在工地上干活,就是【并行】(Parallelism)。
在软件系统中,【程序】是否能【并行】运行,要看物理上有多少个CPU核心可以同时干活(或者再扩展一下,有多少台可用的物理主机)。
比如你写了个Java程序,同时启动了4个线程,但CPU只有单核,那么同一时刻只有一个线程在运行。如果有4个CPU核心,那么可以做到4个线程完全【并行】运行。如果有2个核心,那么就处于一种中间态。比如你可以用“并发度=4“,”并行度=2“形容这种情况。
为啥要并发
把事情设计为【并发】有什么好处呢?假如能同时干活的人只有1个,其实并没有什么好处。【并发】的方法的总耗时总会>=串行的方法。因为【并发】或多或少总会引入需要协作和沟通成本。最小的代价就是不需要沟通,此时【并发】的方法和串行的方法工作量是一样的。
但是【并发】的巨大优势是在可以干活的人数量变多时,马上得到【并行】的好处。假如我们可以得到一个【并发】的【P进程】,并且真的为其配备足够多的人,那么做事的效率就会高很多。回到软件系统,假如有一个【并发】的【程序】,它在只有1个CPU的核心的机器上可以跑,在2个的CPU的也可以跑,在4核CPU上也可以跑。物理上可用CPU核心越多,程序能够越快执行完。而不管在哪里跑,程序本身不用做变化。编程是一件成本很高的事,能够做到程序不变而适应各种环境,可以极大的降低开发成本。你能想象下为1核心CPU开发的Office软件和4核心的不一样吗?
并发和并行的关系是什么
Rob Pike在一个Talk里(https://blog.golang.org/concurrency-is-not-parallelism)提到了很重要的两个观点:
- Concurrency is not Parallelism
- Concurrency enables parallelism & makes parallelism (and scaling and everything else) easy
前一个观点【并发】和【并行】不是一件事,我们都可以理解了。【并发】说的是处理(Deal)的方法;【并行】说的是执行(Execution)的方法。
后一个观点指的是,如果想让一个事情变得容易【并行】,先得让制定一个【并发】的方法。倘若一个事情压根就没有【并发】的方法,那么无论有多少个可以干活的人,也不能【并行】。比如你让20个人不铺路,而是一起去拧同一个灯泡,也只能有一个人踩在梯子上去拧,其他19个人只能看着,啥也干不了。
对于一个问题,能不能找到【并发】的办法,取决于问题本身。有些问题很容易【并发】,有些问题可以一部分【并发】其余的串行(比如对数组排序就是,无论怎么拆,最终也要把每个拆开的问题结果合并到一起再排序才行),有些问题则根本上就不能【并发】。找不到【并发】的方法也就意味着不管有多少CPU核心,也没法【并行】执行。
换一个极端,假如为最多20个人设计了【并发】的方法,结果来了40个人,就意味着40人里有20个人是闲着的,是浪费。也就是说【并行】的上限是由【并发】的方法的设计决定的。这就解释了你吃鸡的时候,4核CPU和8核差别不大,因为这个游戏压根就没设计成可以利用这么多个CPU核心。(BTW,但游戏被设计为能充分利用显卡的多核心)
其实上面只是将CPU核心当作是“做事的人“,再广义一点,比如显卡,网卡,磁盘都是独立的可以干活的人。这些组件之间也可以并行的跑。因此,在设计程序的时候,可以比如把计算和IO任务拆开设计一个【并发】的方法,然后利用CPU和网卡是两个零件来【并行】的跑。
常见的误解
你可能看到过下面的论断:
并发是多个任务交替使用CPU,同一时刻只有一个任务在跑;> 并行是多个任务同时跑 这个理解不能说全错,但是合到一起就形成了错误的理解。这个错误的理解就是:并发和并行是两个并列的,非此即彼的概念,一个状态要不就是并行的,要不就是并发的。这是完全错误的,实际上看到上面的解释你就会发现【并发】和【并行】描述的是两个频道的事情。正如Rob Pike所言,一个是“如何处理”,一个是“如何执行”。因此,对于: 并发是多个任务交替使用CPU,同一时刻只有一个任务在跑 其实正确的理解是:针对一个问题,想到了一个可以拆解为多个【并发】的任务,这些任务执行时因为只有一个CPU只能“切换”的跑。
对于: 并行是多个任务同时跑 其实的意思是:如果这些并行执行的任务是解决同一个问题的,那么他们既是【并发】的,同时也是【并行】的。
那么可不可以做到只【并行】,而不【并发】呢?当然可以,但这也就意味着【并行】的程序之间没有什么关联,各干各的,就像大街上来来往往的陌生人一样。这的确是【并行】,并且是这个世界的常态。但是一群不认识的,各干各的人是不能一起解决问题的,要一起就得有同一个目标,制定一套沟通的方法,形成【并发】的方案。这种形式在现实当中就是“公司”。为什么要这么理解并发
将并发理解为一种解决问题的方法,其主要用意是表达:一个问题的解决方案是可以由许许多多的并发任务组装(compose)到一起的。这有点像OOP里表达一个类可以由其他类的成员组装到一起一样。
将大的任务拆解为许许多多小的可以并发的任务是重要的编程思想。
比如当你在编写一个GET /user/:userId接口时,实际上底层要去3个地方取用户的基本信息(头像、昵称),活动的积分,当前已经下的订单,再组装到一起返回,用nodejs大概可以写成:
const userId = await doGetUserIdByToken(token);
const [userBasic, userScore, userProcessingOrders] =
await Promise.all([ // 并发执行下面3个任务
doGetUserBasic(userId),
doGetUserScore(userId),
doGetProcessingOrders(userId)
]);
const user = {...userBasic, ...userScore, ...userProcessingOrders};
return user;
这段代码表达的就是这样的流程:
如果把一个并发任务以函数的方式去写就刚好把函数式编程(FP)与并发编程结合起来,就容易得到写起来很舒服,并且有利于并行执行的代码。这也是为什么很多FP语言都天然很适合做并发程序设计的原因。知道了这些如何做事
我们做事的最终目标是1)能够得到正确的结果;2)能够尽量高效。高效有两个手段:一是优化做事的办法,这相当于改进算法,比如排序用快排而不是冒泡排序,这一点本文就不赘述了;另外一种方式就是让多个worker【并行】干。而为了【并行】,必须先得找到一个【并发】的方案。
我把这个思路的流程画成一张图供大家参考。
如果你理解了我在说什么就会发现,不管是写程序还是做任何事情,关键点是想到一个好的做事办法,一个可以Scale的,未来如果资源足够可以容易扩展到并行的办法。有了这个办法,具体怎么实施,用什么工具是次一级要考虑的问题。高并发
最后再说说【高并发】。其实【高并发】的意思和前面说的【并发】的意思不止是差了一个“高”字,而是个宽泛得多的概念。【高并发】是指可以让软件系统在一段时间内能够处理大量的请求。比如每秒钟可以完成10万个请求。这是互联网系统的一个重要的特征。
不像【并发】说的是“处理”,【并行】说的是“执行”,【高并发】说的是最终效果。只要能达到效果,不管怎么实现都行。因此,极端一点【高并发】甚至并不一定需要【并行】,只要处理速度快的足够满足要求就可以。如启动一个nginx的【OS进程】,它只能用到一个CPU核心,也就不可能【并行】。但是他如果能每秒能处理10万个请求,而业务需求只要求8万个请求就可以了,那么这个单进程的nginx本身就算【高并发】了。 有时我面试别人的时候,对方简历上写做过高并发。仔细一问才发现只不过使用了nginx或者redis这种性能表现很好的系统实现功能。其实并没有做什么困难的工作。这样的同学写简历时一定要慎重,吹水是没有好结果滴。 当然,现实当中【高并发】的要求会相当“高“(双十一都刷过吧),说的也是完整的业务流程请求,而非简单的HTTP转发。这样的系统大量应用各种【并发】的集中人类智慧的各种方法,并尽可能的【并行】。
除了【并发】和【并行】,【高并发】还需要:
- 数据表普遍被分库分表,否则单机放不下,或者查询性能不足
- 解决分布式事务
- 因为机器都可能坏,为了保证少数机器坏掉不会影响处理的性能,必须引入HA机制
- 因为系统都有极限,超过极限响应能力就会急剧下降。因此必须引入限流的方案来保护系统
- 这么复杂的系统会涉及到N个service,N个存储,N个队列…… 这些资源的管理又成为了新的问题,这又需要对集群和服务做管理
- 这么多服务,肯定要解决分布式的Tracing和报警问题
- ……
当面试的时候提起【高并发】,大概率是希望面试者聊聊上面这些主题。但请特别特别留意,不同领域的【高并发】实际的意思(怎么算“高”,如何达成,哪些问题是关键问题)会非常不同。电商的高并发,抖音的高并发,12306卖火车票的高并发,基金交易系统的高并发,海量数据处理的高并发,这些问题其实都很不同。所以我很建议每次都讨论具体的问题,而非泛泛谈论【高并发】这个名词。
商业世界的高并发
拓展一下,从商业上考虑【高并发】,其实际的意思是“用尽可能少的资源实现足够满足需要的并发请求数量,以形成竞争优势“。能用有限资源短时间内处理大量请求,也就意味着:
1)单个请求处理成本的降低。比如传统企业处理一单交易成本是10元,而互联网企业压低到了0.1元。这就形成了“规模经济下的低成本结构“,是一种碾压式的竞争优势。
2)提高转化效率。为了获客,市场部门都会拼命做如做拼团、发红包的工作。假设两家公司花同样的预算做获客。公司A的下单系统只能支持1000单/s;而B公司能做到成本不比A公司多很多的情况下实现10000单/s,那么过一段时间,A公司将被彻底打垮。如果你是老板,并且对用户需求很有信息,你会玩命砸技术投入,避免系统成为商业闭环的瓶颈(如果发生了,真坑啊)。
这也就是为啥有些公司突然火起来,然后玩命招技术人员。而做技术的同学能够有工作机会的原因。
但如果【高并发】并非是一个公司的商业闭环的关键问题。公司的商业价值是建立在客户关系之类的事情上,或者单笔交易金额比较大,没必要搞很多用户(比如卖保险)。就没有必要在技术上投入大量资源了。相反,聘请许多好的销售,公关人员才是更重要的。我想你一定看过房产中介公司每天早上喊口号对吧。因此,想要在技术上精进的同学最好也要避免去那些公司。不管在哪里做事情,一定要保证自己做的直接和商业价值挂钩的事情才能有更多机会成长。
恭喜你
恭喜你看到这里,因为你已经打败了世界上99%的用户。非常高兴你没有被讲懵逼。但为了验证一下你到底懂没懂,我这里有个问题,请不要打我:)
本文中到底提到了哪几种Process?分别都是什么意思?
答案:共3种
- 表示“做事方法”
- 操作系统里表示程序执行实例
- Erlang语言中的并发单元,彼此相互隔离,又俗称“Actor”
圣诞快乐