[ ECUG 专题回顾]《再谈 CERL:详论 GO 与 ERLANG 的并发编程模型差别》-许式伟

许式伟:咱们开始,先介绍一下ECUG,从07年开始,最先在珠三角珠海广州深圳,在珠三角兴起,最先是Erlang的社区。大概到10年的时候改名为实时效云计算的群组,最先的时候也不局限于Erlang,而是会有各类语言如Haskell、Scala等..,其实根本就没有限制,只要是中途穿插后端开发运维的实践均可以,后来咱们就正式更名为实效云计算的群组。,范围扩也蛮大到全国,基本上北京、长三角都有举办过。因此应该说到今天坚持了也差很少有8年,总共有9届,07年的时候办了2届。这个是ECUG 的历史。南京是第一次办这个大会,我大学是在南京念的。今年想的挺久的,咱们但愿是可以把这个火花在全部的城市都可以点燃,因此今年就选择了南京这样一个对我来讲比较有特殊意义的地方。 程序员

我开始个人话题了。其实,这个话题我其实在杭州的ECUG上时候讲过,可是当时讲的比较婉约,实际上我当时已经意识到Erlang的编程风格的问题,可是解刨得并不完全,,因此我今天又回头谈一下这个话题,用相对比较详细的方法去对比说Go与Erlang并发模型二者到底有什么不一样?由于基本上我知道ECUG 发展历史的人都有困惑,为何我会从Erlang转到切到GoO。 算法

这个是话题的来由仍是想从头谈谈GO和Erlang的并发,我在09年开始决定放弃用Erlang本身在C++里面从新造一个Erlang的编程模型,CERL这个网络库最初的出发点是这样的,因此CERL的C表明的是C/C++、ERL表明的是Erlang。最先的思路是简单把Erlang搬到C++,由于Erlang的程序员确实比较难招,可是后来发现其实Erlang的并发模型并无我想象得那么舒畅,就搞了一个CERL2.0,它是对Erlang模型的反思和改进。最后发现这个反思最终获得的结果和Go的并发模型彻底一致。因此CERL1.0和2.0的对比,其实你能够认为是 Erlang 和 Go 的对比。其实不少人问我这个话题,为何CERL没有开源?缘由是我以为过了那个时间点了,开源没有太大的意义,因此我以为不太想误人子弟,由于我本身最先是C++的粉丝,可是我接触Go之后有一个很是强烈的愿望我但愿C++这样的东西最好仍是可以早点退出历史舞台,关于这个话题我曾经有一个演讲,是讲Go与七牛的历史,我反思了个人C++奋斗史,那个演讲我会后给你们做为一个补充的材料放上去。 编程

既然没有开源那应该怎么理解CERL呢?其实这个世界有相似的东西,这个Pth是我最近才知道的,中途还看到过另外一个开源库,大概在08年开源的,惋惜我一时找不到项目网址了。当时我看了一下跟CERL差很少,可是没有CERL库写的完整,可是Pth这个库出现历史是很是早的,并且是GNU下的一个开源项目,它是99年就已经起动了,到06年左右就再也不更新了。它的出现时间很是早,并发模型和CERL是几乎同样,并且完成度很是高,毕竟发展了7年,因此要理解CERL其实也是研究一下这个库基本上就差很少了。可是我其实有一个反思,为何这个Pth这么好的东西为何没有流行起来?第一个是生不逢时,出现的太早因此没有引发注意,由于其实大概谈多核时代这样的概念在个人印象当中是07年左右开始有这样的概念,Erlang也是那个时候才逐步被人意识到价值的。第二个就是否是标准库,由于这样一个库侵入性是很是强的不光你意识它好还有别人也意识到,不然有一个问题别人写库你是不能用的,这个就是侵入性和传染性,因此会致使其实没有办法真的把这个库用起来,这种有侵入性和传入性的库最初兴起的时候须要有一些激发的条件,它没有这样的条件。这和 C/C++ 中 GC 比较难流行是相似的道理,由于 GC 也有侵入性和传染性。 第三个是从实现讲,仍是有瑕疵的,最大的瑕疵就是轻量级进程并非真的轻量。轻量级进程的核心不仅是要性能好, 更重要的是资源占用要小,但多数状况下的这种资源占用小这个实际上是比较难实现的, Go 在这一点作的比较好,有栈的自动增加,最小的栈最初的时候能够只有 4K , 这样每一个轻量级进程从资源占用来讲真的很轻量。 可是要达到这一点这个绝大多数的库都很难作到。像CERL咱们只能作到说你本身指定说这个轻量级进程栈要多大,可是对程序员来讲指定栈大小是很是困难的事情,有很大的心智负担。要理解CERL,研究这个Pth是比较好的学习材料,固然第二个我认为就是直接学习Go的Runtime了。从轻量级进程来说,它的底层跟CERL是同样的。 后端

轻量级进程模型我很是早就提了,从我最初提倡Erlang的时候就已经提出了这个概念,什么是轻量级进程模型呢?很简单就是两个,一个是鼓励用同步IO写程序逻辑,第二点是用尽量多的并发进程来提高IO并发能力。这和异步IO并发模型不同的,哪怕你是单线程也能够作高并发。 服务器

全部轻量级进程并发模型的核心思想都是同样的,第一让每一个轻量级进程的资源占用更小,这样就能够建立百万千万级别的并发,可以建立的进程个数惟一限制就是你的内存。每一个进程资源占用的越小可以产生的并发能力就越高。作服务端你们都知道,内存资源是很是宝贵的资源,但某种意义来讲也是很是廉价的。第二就是更轻的切换成本,这是为何把进程作到用户态,这个和函数的调用基本是在同一个数量级的,切换成本很是很是低。可是若是是操做系统进程则至少要从用户态到核心态再到用户态的切换。 网络

讲一下轻量级进程模型的实现原理,这个是蛮多人仍是比较关注的。我以前比较少谈这个,可是我今天咱们详细的谈一谈轻量级进程究竟是怎么回事。先谈谈进程,所谓的进程究竟是什么样的东西?其实进程本质上无非就是一个栈加上寄存器器的状态。进程的切换怎么作呢?就是保存当前进程的寄存器,而后把寄存器修改成另一个新进程的寄存器状态,这样至关于同时也切换了栈,由于栈的位置其实也寄存器维持的(ESP/EBP)。这个就是进程的概念,哪怕操做系统的内核帮你作的本质上也是这样。因此这些事情是在用户态同样能够作到,而不是不能作到。本质上来说和函数的那个调用你能够认为也是差很少,由于函数的调用也是保存寄存器,只是相对少一些,至少不会切换栈。因此本质上讲实际上是这个切换的成本是和函数调用是基本上差很少的,我本身测过,大概就是函数调用的10倍左右,基本仍是在一样的数量级范畴。那么在这样一个轻量级进程的概念引入之后,实际上整个轻量级进程的程序物理上是怎么样的?底层其实仍是线程池加异步IO,你能够把这个线程池中的每一个线程想象成虚拟CPU(VCPU)。逻辑的轻量级进程(routine)的个数一般是远大于物理的线程数的,每一个物理的线程同一个时刻确定只有一个routine在跑,更多的routine是在等待当中的。可是这个等待中的routine有两种,一种是等IO的,就是说我把CPU交给他也干不了活,还有一种是IO操做已经完成,或者是本身自己并无等任何前置条件,总之是能够参与调度的。若是某一个物理的线程(VCPU)它的routine主动的或者是由于IO触发了一个调度,把线程(VCPU)让出来,这个时候就可让一个新routine跑在上面,也就是从等待当中而且能够知足调度的routine参与调度,按照某种优先级算法选择一个routine。因此轻量级进程调度原理是这样的,它是用户态的线程,而后有一个非强占式的调度机制,调度时机主要由IO操做触发。发生IO操做的时候,IO操做的函数是这样实现的:首先发起一个异步的IO请求,发起后把这个routine状态设置为等待IO完成,而后再出让CPU,这个时候也就触发调度器的调度,这个时候调度器就会看看有没有人等着调度,有它就能够切换过去。而后再IO事件完成的时候,IO完成后一般会有一个回调函数做为IO完成的事件通知,这个会被调度器接管,具体作什么呢?很简单就是把这个IO操做所属的routine设为Ready,能够参与调度了。由于刚刚它的状态是在等IO,就算调度到它也没有办法作事情。而 Ready 的话就是让这个routine能够参与调度。还有一种状况就是routine主动出让CPU,这种状况下routine的状态在切换的时候仍然是Ready的,任何的时间均可以切到它。以上几个基本上是非强占式的调度里面最基础的几个调度器触发的条件:IO操做、IO完成事件、主动出让CPU。可是其实在用户态的线程也能够实现强占式的调度,作法也是很是简单的,调度器起来一个定时器,这个定时器定时出发一个定时任务,这个定时任务检查每一个正在执行当中的routine的状态,发现占CPU时间比较长就可让它主动地让出CPU,这就就能够实现强占式的调度。因此哪怕在用户态,它能够彻底实现操做系统进程调度全部作的事情。这就是轻量级进程的实现原理。 并发

下面一个问题是Erlang和Go到底有什么不一样?这两个不都是轻量级进程的并发模型?应该说它们的基础哲学确实差很少,可是细节上有很是大的差别,而不是一点点的差别。主要的差别是在于几点:第一个对锁的态度不同,第二个对异步IO的态度不同,第三个不算最主要的细节,可是是次重要的细节,二者的消息机制不太同样。 运维

首先谈谈对锁的态度,Erlang 对锁很是反感,它认为变量不可变能够很大程度避免锁,Erlang 认为锁有很大的心智负担因此不该该存在锁。 Go 的观念是锁确实有很大的心智负担,可是锁基本上避无可避。咱们先宏观看看锁为何是避无可避的,首先服务器首先是一个共享资源,是不少用户在用的,不是为某一我的用的, 因此服务器自己就是共享资源, 一旦有并发就是这些并发请求就在抢这个共享资源。咱们清楚, 一旦有人共享状态而且相互强占去改变它的话,这个时候必然是有锁的,这点是不以技术的实现细节为转移的, 固然这个分析是从宏观角度讲,后面我还会讲技术细节,来谈锁为何不能够避免。 异步

Erlang为何没有锁呢?实际上Erlang的服务器是单进程(Process)的,是逻辑上就无并发的东西。一个Process就是一个执行体,因此Erlang的服务器和Go的服务器不同,Go的服务器必然是多进程(goroutine)一块儿构成一个服务器的,每一个请求一个独立的进程(goroutine)。可是Erlang不同,一个Erlang服务器是一个单进程的东西,既然是一个单进程的首先全部的并发请求都进入了进程邮箱(后面会谈这个进程邮箱),而后这个服务器从进程邮箱里面取邮件(请求的内容)而后处理,因此Erlang的单个服务器并无并发的请求,这个是他不须要锁的根本缘由,其实并非由于它没有变量,变量不可变这些。由于你们都知道单线程的服务器必定是没有锁的。那么可能会有人问,那Erlang怎么作高并发呢?实际上是两点:第一是每一个Erlang物理的进程会有不少的服务器,每一个服务器相互是无干扰的,它们能够并发。第二是单服务器想要高并发怎么办?Erlang对这个问题的回答就是请异步IO。 函数

可是异步 IO 给 Erlang 带来了什么麻烦呢?首先是服务器状态变复杂了,这个复杂是很是很是要命的,这致使我最后认为 Erlang一旦引入了异步 IO 以后,其实比正统的异步 IO 编程模型还要糟糕。咱们看几点。首先为何会有中间状态的引入?由于有异步 IO,因此刚刚的某一个请求其实尚未完成,可是它必须把时间让给另一个请求,因此这个时候服务器就要维持刚刚没有完成的那个请求的中间状态。一旦有中间状态的话,这个服务器的状态自己就不干净,单次请求的中间状态要服务器来维持状态,这个是很是不合理的事情。第二,这个服务器的中间状态将致使比较复杂的状态机,这里面的状态很复杂,由于服务器不仅是要维持一个请求的状态,而是全部的未完成的请求的状态都要它来维持。第三,这些中间状态会致使有锁的诉求,为何会有锁的诉求我下面会讲。因此Erlang虽然试图避开锁,可是一旦有异步 IO 其实本质上仍然没有办法避开锁。

为何Erlang没有避开锁呢?刚刚咱们已经讲了,本质上讲是由于有进程邮箱的存在,并且Erlang的服务器是单进程(执行体),因此常规上没有并发因此不须要锁,可是一旦引入了异步IO之后就会有伪的并发。既然是单的进程,不可能真的有并发,但若是咱们把Erlang的进程(Process)也是认为一个VCPU,由于有请求没有完成,因此同时就有不少并发请求在同一个VCPU上跑。这中间可能出现某个请求须要暂时占用某种资源是不能释放的,会出现一些相互互斥的行为。一旦有这样的行为就必然有锁,这个锁虽然不是操做系统实现而是本身实现,具体可能会体现为相似BusyFlag这样的东西,这其实就是锁。全部锁的特征,好比说忘记把这个释放了,整个服务器就被挂住了,它的行为和全部的锁的行为是彻底同样的。有人会说我根本没有操做系统锁,的确单线程的程序必然不会有操做系统的锁,可是不能怀疑其实咱们代码里面是有锁的。

因此,在对锁的态度这个问题上,Erlang竭力避免锁,可是实质上只是把锁的问题抛给用户。而Go则选择接受了锁没法回避的事实。

咱们再看对异步IO的态度。Go认为,不管如何都不该该有异步IO的代码。而Erlang从轻量级进程并发模型来讲不是很纯粹,它没有排斥异步IO,是一个混杂体,是异步IO编程加上轻量级进程模型的混杂,这个混杂的结果是让Erlang的编程,一旦用了异步IO的话,实际上是比单纯的异步IO编程的心智负担还要大。

最后一个细节是我刚刚讲过的次重要的概念,它是 Erlang的进程邮箱,全部发给Erlang进程的消息都会发到这个进程邮箱,Erlang提供邮箱收发消息的元语。Go则提供了channel这样的通信设施,这个channel能够轻易建立不少个,而后用它进行进程通信。相比之下,Go的消息机制抽象更轻盈。消息队列和进程是彻底独立的设施。

那么,咱们再看看咱们应该如何去理解Go的并发模型?Go的并发模型很新吗?其实不是的。我在不少的场合都讲过,Go的并发模型其实根本不是一个创新性的东西,为何呢?由于Go的并发模型是从有网络以来咱们就是这么写程序的,从第一天写网络程序的时候咱们写的就是Go推崇的并发模型。那么问题在哪里呢?为何你们最后放弃了最古老的并发模型?缘由是由于OS的进程和线程过重,致使了你们人们去千方百计提升IO并发的时候用了一些歪招,也就是今天你们普遍接受的异步IO编程范式。这个异步IO变成范式带来的问题是程序员的编程心智负担大大加剧。因此Go的创举有两点:第一点就是价值回归,其实最古老的并发编程模型就是最好的并发模型。它的问题是执行体的成本,因此Go的最重要的事情就是让执行体的成本无限下降,你们知道Go的最新版本栈最小能够到4K,小到让不少人以为难以想象。因此这一点Go实际上是从实现层面解决的,而不是从编程范式解决的。Go第二个创举是让执行体变成了语言内建的标准设施,刚刚我说那个Pth库流行不起来是由于这种并发模型是有传染性和互斥性的,这个系统当中不该该有两个这样的设施,而若是你们用的设施不同,它是会排斥的,这个传染性必需要求执行体必须成为标准化的东西。并且这已是什么年代了?多核时代已经喊了快十年了,可是咱们你们能够看到,几乎没有多少语言把执行体这个做为语言内建标准来作,我以为这是Go很大的创举。

让咱们回顾一下,Go的并发模型其实就是这一页提到的东西。它是最古老的并发模型。现代的操做系统,以及你们学的操做系统原理,和Go里面的概念彻底一致。首先这个并发模型涉及的是执行体这样一个概念,也就是Go的goroutine,而后一次是原子操做、互斥体、同步、消息,最后就是同步IO。这些就是Go的并发模型全部包含的内容。

那么最后一个问题,Erlang中是否是能够实施Go的并发模型?在Go里面实施Erlang的并发模型是比较容易的,可是反过来想Erlang里面可不能够实现Go的并发模型呢?原则上是不能。由于在Erlang当中进程不能实现共享状态,这个是他反对锁的最重要的基点。进程不能共享状态,因此不用锁,但其实我认为这个是最大的问题,为何呢?由于Erlang收到请求之后没有办法建立一个子的执行体,而后让它处理某一个具体的请求不用再管它。可是Erlang里面进程没有共享状态,你要改服务器状态必须用异步IO的方式,把事情作了再把消息扔给服务器对他说你本身改状态。经过消息改服务器状态,这个成本是比较大的,并且带来了不少问题。因此我认为Erlang的用消息改这个状态是很差的作法,绕了一大圈没有本质改变任何的东西。固然,若是我在Erlang里面非要作到Go的并发模型也能够,这须要对Erlang作一个阉割,若是咱们让Erlang的服务器都无状态的话,是能够实施Go的并发模型。什么样的服务器是无状态的?你们可能很容易想到PHP服务器。它把状态交给全部的外部的存储服务,由存储服务来维持状态。若是说Erlang的服务器是无状态的是能够实施Go的并发模型,由于全部的状态都经过修改外部的存储。可是这样的话Erlang程序员确定是很伤心,看起来Erlang语言并无带来什么实质性的好处。因此个人结论是:是时候放弃Erlang了。

这就是个人演讲内容,谢谢你们!

相关文章
相关标签/搜索