回“老赵”关于“Erlang中最大的问题”
活跃在博客园的“老赵”,是一位研究 .NET 非常深入的同学(因为我本人也是老赵——jackyz.zhao,所以,特地加了引号)。他最近很关注“在 .NET 下实现 Erlang 语言特性”的课题,并为此写了一系列的技术文章,相当不错,我一直都在关注。他自己写了一个名为 ActorLite 的小东西,此前做过介绍,是个不错的尝试。
最近“老赵”同学写了一篇《一种适合C# Actor的消息执行方式(上)》,其中提到“(在消息执行上) Erlang 中最大的问题”。这是一个很有意思的观点,而且因为富于代表性因而显得很有价值,很有必要拿出来和大家探讨。
其中提到:
Erlang的优势与缺陷
Erlang在消息执行方式上的优势在于灵活。Erlang是弱类型语言,在实现的时候可以任意调整消息的内容,或是模式的要求。在 Erlang进行模式匹配时往往有种约定:使用“原子”来表示“做什么”,而使用“绑定”来获取操作所需要的“数据”,这种方式避免了冗余的cast和赋值,在使用的时候颇为灵活。然而,世上没有完美的事物,Erlang的消息执行方式也有缺陷,而且是较为明显的缺陷。
首先,Erlang的数据抽象能力实在太弱。如果编写一个略显复杂的应用程序,您会发现程序里充斥着复杂的元组。您可能会疲于应对那些拥有7、 8个单元(甚至跟多)的元组,一个一个数过来到底某个绑定匹配的是第几项,它的含义究竟是什么——一旦搞错,程序便会出错,而且想要调试都较为困难。因此,也有人戏称Erlang是一门“天生会损害人视力的语言”(令人惊讶的是,那篇文章居然搜不到了,我们只能从搜索引擎上看出点痕迹了)。
而我认为,这并不是Erlang语言中最大的问题,Erlang中最大的问题也是其“弱类型”特性。例如,现在有一个公用的Service Locator服务,任意类型的Actor都会像SL发送一个消息用于请求某个Service的位置,SL会在得到请求之后,向请求方发送一条消息表示应答。试想,如果SL的功能需要有所修改,作为回复的消息结构产生了变化,那么我们势必要修改每一个请求方中所匹配的模式。由于消息的发送方和接受方在实际上完全分离,没有基于任何协议,因此静态检查几乎无从做起。一旦遇到这种需要大规模的修改的情况,Erlang程序便很容易产生差错。因为一旦有所遗漏,系统便无法正常执行下去了。
这的确是对于动态类型语言很常见到的担心,而且,确实,如果不加注意会成为严重的问题。这种困扰确实是因为 Erlang 的“动态类型”和“基于消息”而造成的。但,这并非无解,实际上,在 Erlang 编程规范之中,已经给出了解决方案。
通常而言,Erlang 中的消息应该是以“控制流”为主,在消息的数据结构表达上,不同的消息通常都对应着不同的处理流程,在这里“控制”是消息中最为关注的内容。为此我们可以简单的引入一个 atom 来予以表达,这就是 Tag Messages 的最佳实践:
5.7 Tag messages
All messages should be tagged. This makes the order in the receive statement less important and the implementation of new messages easier.
Don’t program like this:
loop(State) ->
receive
...
{Mod, Funcs, Args} -> % Don't do this
apply(Mod, Funcs, Args},
loop(State);
...
end.The new message {get_status_info, From, Option} will introduce a conflict if it is placed below the {Mod, Func, Args} message.
If messages are synchronous, the return message should be tagged with a new atom, describing the returned message. Example: if the incoming message is tagged get_status_info, the returned message could be tagged status_info. One reason for choosing different tags is to make debugging easier.
This is a good solution:
loop(State) ->
receive
...
{execute, Mod, Funcs, Args} -> % Use a tagged message.
apply(Mod, Funcs, Args},
loop(State);
{get_status_info, From, Option} ->
From ! {status_info, get_status_info(Option, State)},
loop(State);
...
end.
可以相信,一段庞杂的代码,在经过一番这样对 Message 本身的设计和重构之后,最终都能让其恢复“秩序与美感”。而消息中的其他参数,如果其处理逻辑非常复杂的话,那么带有模式匹配的子函数,又或着是 Tuple/Record 正是其用武之地。
上面我们提到了“对 Message 的设计和重构”,实际上,这只是问题的一个方面。另外一个方面是:无论 Message 的“协议”设计得有多精巧,其对外的接口都不应该用消息来表达,而应该是“接口函数”的形式。
Erlang 系统之中的消息是极度灵活的系统,但它并不适合用来作为模块之间的接口,因为它过于灵活。更加适合这一角色的设施是“函数”。用来充当这样角色的函数就被称作“接口函数”,在其实现代码中,除了“按照约定的格式发消息以外”什么别的也不做——因为它包装的正是以消息为载体的交互“协议”。它是函数,有着严格的语法检查,而且可以在多个模块之间重用。一旦需要修改消息协议,起隔离作用的“接口函数”就会体现出其存在的巨大价值。
5.10 Interface functions
Use functions for interfaces whenever possible, avoid sending messages directly. Encapsulate message passing into interface functions. There are cases where you can’t do this.
The message protocol is internal information and should be hidden to other modules.
Example of interface function:
-module(fileserver).
-export([start/0, stop/0, open_file/1, ...]).
open_file(FileName) ->
fileserver ! {open_file_request, FileName},
receive
{open_file_response, Result} -> Result
end.
...code...
Erlang 是动态语言,对其进行静态检查比较困难(并非不能 R13 已经有所动作)。但,并不是说没有静态检查就会寸步难行。Erlang 同样重要的语法特性是它还是函数式语言,遇到有疑惑的地方,不妨以函数的角度来思考。
感谢 Arbow 的分享。感谢 JeffreyZhao 的写作。
Erlang 语言素有“难学”的名声,其中一个原因就是因为虽然其语法十分简单,但常会让人心生 “就这样了,然后呢?” 之惑——因为过于灵活而无所适从,而且也不存在着显而易见的正确用法。这种困扰通常要在有了一定的实践经验之后才会渐渐消散。换句话说,在“学会”和“用好”之间存在着一堵模模糊糊的墙,而这一“未知区域”又需要一定的耐心和经验方可穿越。
多谢补充!
我对Erlang的了解仅到“知道可以做什么”,“怎么做”,除此之外还没有任何实践,有什么问题多多指出。:)
我的理解是,tag message即使用atom来标记“做什么”,Programming Erlang里也始终贯彻这一点,但是它还是无法约束做这个事情需要哪些“数据”。
例如open_file函数,它需要receive {open_file_response, Result},但是如果以后它要修改为receive {open_file_response, Result, NewResult},这样所有调用open_file函数的模块都需要修改了。
我的文章也主要是指这个意思,不知道在实践中该如何应对这个情况。
@Jeffrey Zhao
这种情况正是接口函数发挥作用的地方。你的用例中发生变更的是通讯协议,而接口函数正是封装通讯协议的地方。向用户暴露接口函数而不是通讯协议,同时尽可能在不更改接口函数的情况下修改协议,一定程度上可以解决你的问题。作为用户,不应该去试图了解通讯协议并按照通讯协议去自行编写客户端函数,而是应该使用接口函数来访问服务。如果接口函数也不能保证不变性,我想那应该就不是Erlang的问题了,而是如何设计稳定、可扩展API接口的问题了。jackyz.zhao应该也是这个意思吧?
@liancheng
那么,对于我提出的情况,一个良好的设计应该是什么样子的呢?例如我现在的确修改了通讯协议,但是这有时候是不得不修改的。事实上对于Erlang这种没有严格约束的语言,任何process之间的通讯都是在约定,如果有一方要改变约定,又该如何是好呢?
再者,例如这个示例,open_file的作用是“发起”操作,因此我们可以用接口函数。但是我们的“应答”是被动的,开发人员又如何准备另一个接口函数,用于接收应答消息?
实在是忍不住了,说两句吧,呵呵!
1、Erlang是强类型语言,超强类型的语言,可以去google一下弱类型的定义。receive时是基于pattern matching进行消息接收的,这个有个学名叫做selective receive,如果没有做过超级复杂的状态机,是根本无法理解其在保证状态机概念完整性和防止状态爆炸方面所带来的巨大优势的,我记得Ulf Wiger还专门写过一个PPT。另外,pattern matching也使得OO语言中所谓的多态语法变得无比丑陋,繁杂。
2、如果你用imperative的视角去应用functional世界中的设施,当然会觉得别扭,元组中项的多少和数据抽象也根本没啥关系。再说了,由于自己问题没有理解清楚,搞那么多项出来,咋能够怪Erlang呢。说道调试,我编Erlang程序也有n年了(n>4),还从来没有怎么调试过程序,为啥?因为首先使用Erlang编程很少出bug,即使出了,大部分的bug就在出错的地方,一目了然,而不像其他语言中的距离十万八千里。建议看看《purely functional data structures》这本书。
3、实在是不理解举得那个Service Loader的问题是想说明啥,基于消息通信的双方必然基于一定的协议(不管你有没有意识到),这个协议是脱离语言的,包括语法和语义两部分构成。在实现时,语法部分反映到语言的结构定义上,语义部分反映到流程控制中。更改数据结构其实就是更改了协议,由于消息通信是一个运行时的动态过程(还涉及到消息的marshalling这种低级操作),所谓的静态检查也起不了多大的作用。这个问题是一个系统设计问题,也就是协议的语义检查和版本兼容设计问题,协议改了,就等于是协议版本升级了,此时更应该考虑的问题是如何平滑的进行协议升级和协议版本兼容的问题,因为可能已经有成千上万的老节点在运行着呢。Joe Armstrong一直在致力于protocol check的工作,甚至后悔当时没把这个作为first class的语言特性(可以google)。我还没见过那个语言、系统平台在这个问题上的支持能超过Erlang/OTP的的。说道出错,Erlang最不怕的就是出错,因为其就是容错的,出错立即重启出错的进程,分析错误,更改,热升级,一气呵成。换成其他语言平台,早玩完了。这就是Erlang的哲学,承认永远无法避免错误,所以造就了Erlang的超级容错特性
最后再说两句,最近出现了很多模拟erlang并发特性的探讨和实践,我想说的是,如果不能理解并做到Erlang对于软件容错的支持,这些模拟只是个皮毛形式罢了。
同样是FP语言, HASKELL比ERLANG优雅, 能否用HASKELL实现 Erlang 语言特性呢?
@sw2wolf
其实functional不functional不是Erlang的重点,按照Joe Armstrong的话来说,Erlang最核心的就是其基于软件容错之上的,pure message passing style的进程模型,进程就是个黑盒子,里面放啥都行,只是放了个functional语言更美味一些,呵呵。
@dsun
1、能不能给出相关资料呢?我查到的资料,没有一个不说明Erlang是弱类型语言的。
2、Erlang的抽象能力差也是公认的吧,即时是Programming Erlang的例子里,也见过元素数量很多的元组。至于bug方面,我没有敢想,就不发表意见了,呵呵。
3、其实你说的和我是统一的,Joe一直致力于protocal check,后悔没有将其作为first class语言特性,不就是说明Erlang这方面有缺陷吗?当然,可以方便的进行热升级,的确是Erlang内置的优秀特性,其他平台虽然并非说做不到,但是的确不如内置平台特性高效。
最后你的看法我非常同意,其实我认为“容错”才是Erlang的关键,而“并发”部分其他语言通过简单的框架都可以实现。
晕死,出去一天没上网,这么热闹的回帖都错过了。呵呵。
@liancheng,就是你说的这个意思。
@Jeffrey Zhao,照你的这个例子,我再罗嗦一下。
例如 open_file 函数,它需要 receive {open_file_response, Result},但是如果以后它要修改为 receive {open_file_response, Result, NewResult},这样所有调用 open_file 函数的模块都需要修改了。
对外而言,你的 Module 所能提供的全部功能就是 Export 出来的那几个 Function ,作为调用者,除了那几个 Function 之外,不需要了解其他的任何信息。
在 open_file 这个例子里,在设计之初,你就需要确定 open_file 会返回什么值,这是接口设计(API)的工作。也就是说,你要在设计阶段确定好这个函数的外部表现是怎样的。具体他是 receive 某个消息也好,或者是代理到其他的模块也好,这都是在实现阶段才去考虑的问题,和他的外部表现无关。一个设计良好的接口,应该是相对稳定的。换句话说,设计出来的接口,应该表述相对稳定的“业务逻辑”,而不是“实现逻辑”。
现在,比如说,你设计好了,最终还是发生了什么事,还是需要改变,这时会有两种情况:
1,内部的协议发生了改变(实现变了),而外部的表现不变(业务表述不变),这时你只管改 open_file 的实现代码就好,外部调用 open_file 的代码你不用改。这是 Interface Function 的要义。
2,外部的表现发生了改变(业务表述都变化了)。这时其实是一个接口的变更,这时,所有调用 open_file 的地方都需要跟着改,这无法避免,无论你在 Java 里也好 .NET 里也好,都是一样的 Erlang 也没什么不同。
这里需要注意的是,此时的接口改变,其内在动力并不一定就是因为协议(实现层面的事情)发生了变化,这两者之间并不存在必然的联系。
让我们更正一个说法:Erlang 是动态类型,而不是弱类型。在实际使用中,确实会大量的碰到往一个元组/记录里塞很多东西的情况,但我认为,这不表示它的抽象能力差,而是相反。而且,如果正确使用语法设施的话,你也不会觉得繁琐。
@Jeffrey Zhao
Joe Armstrong说后悔,是指protocol应该做为语言的first class特性,而不是后来通过库来实现,而且他所关注的是有关protocol的动态语义检查,而不是静态检查。
另外,所谓用接口来屏蔽协议是一个小的设计问题,大家可以考虑一下一个更大范围的设计问题,比如:我们现在有个分布式系统,有N个节点组成,它们之间通过某种协议通信,现在要升级系统,协议也要更改,我们一开始该如何设计protocol和升级流程,以保证可以在系统不停止提供服务的情况下安全、平滑、兼容的进行整个系统的升级呢?
说到底,所谓的高并发、伸缩、在线动态升级其根本都是一个软件容错问题,容错问题解决好了,这些东西都是自然地副产品,否则就只能每个做一套,违反DRY,呵呵。
Erlang是动态类型的,加上一点点的运行时类型检查和分发(通过is_float, is_record等类型检查函数和内生的atom, tuple, List等少量类型)。
Erlang的这一特点决定了Erlang不适用于大型的、具有复杂业务逻辑的企业业务系统。协议、函数的接口在这种情况下时需要经常重构的,也意味着需要被改变,这时就会非常非常头疼。
Erlang的发展方向必然是逐渐加入类型定义,现在是可选项:
http://www.erlang.org/eeps/eep-0008.html
比如:
-spec Function(ArgName1 :: Type1, …, ArgNameN :: TypeN) -> RT.
-type orddict(Key, Val) :: [{Key, Val}].
这些新加入的spec和type属性已经非常接近类型声明了,但,于Java,Scala的类型体系相比,显然是不成体系,以及是不完备的,有些复杂类型,比如继承、mixin的类型无法用这些声明来表达。
在Erlang的mailing list上,甚至开始考虑更多的加入编译期的类型检查,比如:
-fun(a::Type1, b::Type2) -> ….
这样的语言演化与Python的可选类型更接近了。
对于计算密集的任务,Erlang现在还有另一个致命的弱点,就是它的processes的调度机制,是一种大锅饭机制,具体可以看我的blogs上关于scala vs erlang的内容等。
在加一点忠告:如果你不是在开发一个大量的、基于短小的信息交换的应用,那么是时候看看Scala了。在我的那篇blog中:
http://blogtrader.net/dcaoyuan/entry/thinking_in_the_scala_vs
有些结论我没有明说(因为我的测试数据还没有完全准备好),但如果仔细想,就会明白为什么我已经转向Scala了。而且,我对Scala的Actor感觉良好。
haskell的类型系统很好啊! 如果用HASKELL实现类似ERLANG的异步消息系统, 就可以弥补ERLANG动态类型的不足啊?
@jackyz
引用:外部的表现发生了改变(业务表述都变化了)。这时其实是一个接口的变更,这时,所有调用 open_file 的地方都需要跟着改,这无法避免,无论你在 Java 里也好 .NET 里也好,都是一样的 Erlang 也没什么不同。
=========================
的确,任何语言、平台都无法避免接口改变。但是在Java,C#等语言中,可以通过静态检查来进行约束,而Erlang这样的语言却无法做到。这才是我想表达的意思。我并不是说,Erlang会改变接口,而其他语言不会改变。
Haskell的类型系统的确很强大,但是这是否适合Actor模型是一个未知数(包括C# Actor,Scala Actor都有一些问题),因为Haskell毕竟是一个强类型静态检查的语言。Erlang的动态类型特性可以方便的进行开发。不过这种动态类型的确也造成维护上的缺陷,就如同上面Caoyuan所说,对于业务复杂的系统来说,修改和重构是难以避免的,几乎无法避免需要更强大的类型系统。
》Erlang是动态类型的,加上一点点的运行时类型检查和分发(通过is_float, 》
》is_record等类型检查函数和内生的atom, tuple, List等少量类型)。
》Erlang的这一特点决定了Erlang不适用于大型的、具有复杂业务逻辑的企业业务系
》统。
Erlang具有强大的元编程能力的,甚至不次于Lisp,不用来编写企业应用,是因为其最初要解决的问题非常明确,就是超级复杂的电信级系统要求,开发企业应用就是杀鸡用牛刀,Joe的论文应该仔细看上n遍。
》协议、函数的接口在这种情况下时需要经常重构的,也意味着需要被改变,这时就
》会非常非常头疼。
呵呵,第一次听说动态类型语言比静态类型语言难以重构,Ruby/Python要比Java、C++好重构一个数量级以上。再说了,系统部署之后的动态升级是更为复杂的问题,除了Erlang/OTP,还真没有那个语言、平台系统地解决过这个问题。
Philip Walder早就为Erlang开发过一个外挂的类型系统,不过效果不是很好,从工程实践上来看,目前的Erlang还不适合于静态类型系统。
》对于计算密集的任务,Erlang现在还有另一个致命的弱点,就是它的processes的
》调度机制,是一种大锅饭机制。
Erlang是不适合计算密集型任务,它的设计目标本就不在此。但是这和Erlang的process调度机制没啥关系。Erlang的调度的设计目标就是为了达成控制上的软实时,不允许有process占用过长的CPU。
Erlang已经经过数十年的实践检验,且有世界上最复杂的商用系统作为例证。Scala是啥时出来的?
当然,如果不是做真正复杂的系统,其实用啥语言都一样。试想,一个用C++开发了7年都没能做出来的系统,用Erlang三年就出了第一个版本,这是个啥概念?
》Erlang具有强大的元编程能力的,甚至不次于Lisp,不用来编写企业应用,
》是因为其最初要解决的问题非常明确,
》就是超级复杂的电信级系统要求,开发企业应用就是杀鸡用牛刀,
》Joe的论文应该仔细看上n遍。
不懂。你的意思是:这个牛刀是不能用来开发企业应用呢?还是牛刀可以但不必?
》呵呵,第一次听说动态类型语言比静态类型语言难以重构,Ruby/Python要比Java、C++好重构一个数量级以上。
这个我倒是第一次听到。
》但是这和Erlang的process调度机制没啥关系。Erlang的调度的设计目标就是为了达成控制上的软实时,
》不允许有process占用过长的CPU。
所有我说它适合处理短信息交换。处理大的信息块是需要CPU的。
》不懂。你的意思是:这个牛刀是不能用来开发企业应用呢?还是牛刀可以但不必?
当然能,ThoughtWorks已经有人试图在用Erlang开发企业级应用了。试图让人改变已有的思路,或者放弃已经有的东西是很困难的。不过Erlang针对的主要领域还是极度复杂的电信系统设计。
》这个我倒是第一次听到。
可以看看这篇http://www.artima.com/weblogs/viewpost.jsp?thread=4639
》所有我说它适合处理短信息交换。处理大的信息块是需要CPU的。
你的意思是Python之类的语言适合计算密集型的任务了?如果真是这样,Erlang又没有强制你一定得创建成千上万的进程,搞一个进程做计算不就行了吗?
根据印象,google了一下:
Domain Specific Languages in Erlang
http://www.infoq.com/articles/erlang-dsl
http://qconsf.com/sf2008/file?path=/qcon-sanfran-2008/slides//DennisByrne_DSLs_in_Erlang.pdf
Conclusion
I’d like to reiterate my original point about Erlang. This is a fantastic workbench for DSLs. Anonymous functions, regular expression support and pattern matching are only the beginning. Erlang also gives us programmatic access to the tokenized, parsed and abstract forms of an expression. Observe Debasish Ghosh’s string lambdas in Erlang as another example. I hope this article has helped some of you get out of your comfort zones with a new syntax and programming paradigm. I also hope people with think twice before labeling Erlang a specialist language.
》Erlang又没有强制你一定得创建成千上万的进程,搞一个进程做计算不就行了吗?
请再看:
http://blogtrader.net/dcaoyuan/entry/a_case_study_of_scalable
http://blogtrader.net/dcaoyuan/entry/async_or_sync_log_in
根据印象,google了一下:
Domain Specific Languages in Erlang
http://www.infoq.com/articles/erlang-dsl
qconsf.com/sf2008/file?path=/qcon-sanfran-2008/slides//DennisByrne_DSLs_in_Erlang.pdf
@Jeffrey Zhao
关于类型系统你可以看下这篇文章:
http://www.reddit.com/r/programming/comments/63tnv/bruce_eckel_33104_im_over_it_java/c02qx55
erlang是dynamic, strong, latent, structural typing。
c才是弱类型。
看了这个 http://blogtrader.net/dcaoyuan/entry/thinking_in_the_scala_vs,觉得里面的比较过于简单,说明不了啥问题。比如:Process, Actor那个小节中,可曾考虑过和Erlang中selective receive对比?可曾考虑过真实场景中的超级复杂的状态控制如何做?
可以看看这个:
Structured Network Programming
Ulf Wiger
http://www.erlang.se/euc/05/1500Wiger.ppt
试着把里面的例子用Scala实现出来看看
看了这个 blogtrader.net/dcaoyuan/entry/thinking_in_the_scala_vs。觉得里面的比较过于简单,说明不了啥问题。比如:Process, Actor那个小节中,可曾考虑过和Erlang中selective receive对比?可曾考虑过真实场景中的超级复杂的状态控制如何做?
可以看看这个:
Structured Network Programming
Ulf Wiger
http://www.erlang.se/euc/05/1500Wiger.ppt
试着把里面的例子用Scala实现出来看看
此帖及跟帖都太精彩了,erlang大拿们都冒出来了
@dsun
》Erlang又没有强制你一定得创建成千上万的进程,搞一个进程做计算不就行了吗?请再看:
http://blogtrader.net/dcaoyuan/entry/a_case_study_of_scalable
http://blogtrader.net/dcaoyuan/entry/async_or_sync_log_in
多谢改正,的确应该说是“动态类型”。我总结了一下,对于我提出的“Erlang缺少静态检查”主要有两种回应:
1、接口改变的情况比较少,而且Erlang系统还会涉及到大规模的平滑升级,因此静态检查的要求就降低了。
2、Erlang在容错方面的优势,使得即使出错,也很容易发现和改正。
不知道总结地是否正确。
@dsun
》Process, Actor那个小节中,可曾考虑过和Erlang中selective receive对比?可曾考虑过真实场景中的超级复杂的状态控制如何做?
关键是Process, Actor 的调度机制:
Erlang中,每个Process得到CPU的机会相等(所以是大锅饭机制),有些重要的Process或许可以通过设优先级来调控,但这种来者不拒的调度可能导致每个process得到的服务质量都下降。比如,同时有一亿个请求来了,Erlang算了十年(系统没垮,值得表扬)总算算出来了,结果是没有任何一个人等得了,都跑了。
Scala中,真正的服务员是有限的,比如100个,即使来了一亿个请求,也同时只处理100个,其余的排队等候,至少先来的100个人能保证服务质量,剩下的如果还有耐心等,会按队列依次处理,等不及的,或者系统timeout了。
selective receive的确是不可缺少的,而其他平台上设计的Actor API大都缺乏这方面的支持。我在设计的API也对此有所支持。
Scala Actor也已经在容错,和Remote Actor方面下手了,相信不久会有所成果可以体现出来。
所以在那篇比较中,我给出了选择Erlang或是Scala的一种原则:
如果单个任务的处理时间(代价)远大于 process/thread的切换时间(代价),选Scala
如果单个任务的处理时间(代价)约等于或小于 process/thread的切换时间(代价),选Erlang。
对于大信息块的情况,处理一个信息块的时间通常远大于thread切换时间,选Scala是明智的;
对于大量的短信息块,比如,ring测试中简单发一个”hello”,thread/process切换的代价可能更大更显著,选Erlang。
但是,记住,现在JVM的thread切换时间已经是ns级。
@Jeffrey Zhao
关于Erlang的为何采用动态类型可以参看如下两个地方,都有详细的谈及:
http://www.infoq.com/interviews/Erlang-Joe-Armstrong
erlang.org/pipermail/erlang-questions/2006-December/024368.html
@Caoyuan
Erlang的设计哲学不赞成通过优先级别来影响程序的执行,这个完全应该明确的设计出来,这是一个设计问题。如果依赖于底层的调度优先级,这是设计者在偷懒。
Erlang只是说可以支撑大规模的并发,但不能因为此,你就不管三七二十一地搞出这么多并发体来,并发粒度的设计还是得根据问题以及应用场景来定。
@dsun
》Joe的论文应该仔细看上n遍。
Joe的论文我只看了两遍,在我看来,Joe论文的精髓是:
1、并行的任务应该尽量sharing nothing
2、异步消息传递是并行任务间最合适的协作方式。
其余的,包括容错性都可以从这两条中推出来。
》如果单个任务的处理时间(代价)远大于 process/thread的切换时间(代价),选
》Scala
》如果单个任务的处理时间(代价)约等于或小于 process/thread的切换时间(代
》价),选Erlang。
这个我不赞成,如果单个任务的处理时间(代价)约等于或小于 process/thread的切换时间,那么process/thread来进行处理就是个错误的设计,不管用啥语言。
“小消息,大计算”对所有想通过并发提升性能的系统来说都是适用的,和语言无关。
@dsun
》Erlang的设计哲学不赞成通过优先级别来影响程序的执行,
》这个完全应该明确的设计出来,这是一个设计问题。
》如果依赖于底层的调度优先级,这是设计者在偷懒。
这是Erlang能适用的应用场景,但世界上的应用场景是复杂多样的,很多时候我就需要简单地搞定优先级问题,比如在我的另一篇blog中谈到的Erlang中log进程是个singleton process时出现的问题(这个问题Ulf也被搞糊涂过,在mail list上提问),还好,我算是比较快定位了问题所在,要不然就狠很地影响到我们的系统的交付了。
》Erlang只是说可以支撑大规模的并发,但不能因为此,你就不管三七二十一地搞出这么多并发体来,
》并发粒度的设计还是得根据问题以及应用场景来定。
“并发粒度的设计还是得根据问题以及应用场景来定”,开始复杂了吧。在这些场景下Erlang的简单性既然没了,我当然选其它的方案。
@dsun
》这个我不赞成,如果单个任务的处理时间(代价)约等于或小于 process/thread的切换时间,
》那么process/thread来进行处理就是个错误的设计,不管用啥语言。
Erlang就是这么处理的,而且在这一点上正是它的强项呀?
“小消息,大计算”:这是指什么?
很热烈啊,有争议是好事情,哈哈,让我们继续探讨。
@Jeffrey Zhao
首先,我们意识到 Erlang 语言在语法层面并未对“以协议表达接口”进行限制,这是语言的特性,算不上是缺点。然而在工程上,这确实是过于灵活(以至于容易被滥用/引入混乱)的地方。因此,作为最佳实践,才有了“以函数来表达接口,而不是协议”的这么一条。实际上,我们看 OTP 本身的接口设计,它暴露给用户的几乎所有功能接口都是以函数的形式来进行表达的。作为 OTP 的外部使用者,至少我个人目前还没有碰到过需要以某种“协议”的形式去调用某个功能的地方。
“以函数来表达接口,而不是协议”,这是我们往下进一步讨论问题的基础。因为,脱离了这一基础,只会得出“过度灵活的消息会导致接口缺乏约束”的结论。然而,对此大家其实并未存在分歧。
此时,我们会发现,被排除在讨论之外的只是“消息”这个“干扰”因素(因为其他的工业语言没有在语言级别支持这一基础设施)。这个时候再看,在“接口表达方式”上,Erlang 与 Java、 C# 这类静态类型语言实际已经没有太多差异——大家都是要用函数来表达业务逻辑,以函数名称、参数数目和位置(以及对应的类型)这些要素来形成一套描述基本业务逻辑的“DSL”,然后再用这套“DSL”组合出具体的业务逻辑。
我们谈到描述基本业务逻辑的“DSL”,这里面其实只有这么三个要素:1,函数的名称,2,参数数目,3,参数位置以及对应的类型。这在后面的讨论中我们还会再次涉及。如果说 Erlang 与 Java、 C# 在这个问题上有什么不一样的地方,那就是 Erlang 是动态类型语言,而 Java、 C# 这类语言是静态类型语言。这导致了一些差异。
@Caoyuan
将 Caoyuan 兄的观点,按照上述三个要素,以另外一个形式表述,那就是:对于某一个函数而言,Java,C# 这些静态类型语言会在编译和运行时检查函数的名称,参数个数,并根据参数的位置和类型进行检查,判断是否与声明相符。此之为“适合企业业务的严格约束”。而 Erlang 作为动态类型语言,只会在运行时检查函数名称和参数个数(并没有根据位置检查其类型是否与声明相符合的部分)。此之为“缺乏约束,不适合企业业务”。
然而我却得出了与 Caoyuan 兄不同的结论。前面提到了三个要素:函数的名称,参数数目,参数位置以及对应的类型。Erlang 和 Java,C# 在处理上的主要区别在于:1,参数位置以及对应的类型 Erlang 缺乏检查机制。2,编译时 Erlang 不检查函数的名称和参数的数目(这里主要是指在模块之间的调用不做检查,模块内部的调用是做了检查的)。
首先说“参数位置以及对应的类型”,与 Java 和 C# 不同的是,这在 Erlang 里是作为一个语法设施提供给了编程者的(也就是函数的模式匹配)。前面 dsun 也谈到这恰好是从另外一个侧面说明 Erlang 是强类型语言而非弱类型语言的例子。如果说,在其他语言中,这个检查的逻辑是:“判断是否与函数的声明相符”,不符就抛出异常,这是语言提供的唯一行为(而且被认为是好的严格的约束)。那么在 Erlang (以及其他支持模式匹配的函数语言)中,这个逻辑只是被改变成了:“判断是否与函数的某个子句声明相符”,如果相符则执行之,如果全都不符就抛出异常。当然,这一过程只能是放在运行时里。对于这个语法决策,我个人的感觉是,这是一个不错的交易(以不必要的严格换回了灵活)。
此时,如果要在 Erlang 中实现如同静态类型语言一样的严格约束,其实很简单——我们只需要在 Guard 语句中严格的约束调用类型就行了,并且保证每个接口函数都只有这么一个子句。那么,当参数类型不符时,就会直接速错(与抛异常相对应)。然而,即使按照这种方式来编程,以“类型系统”的角度来考量,仍然无法觉得满意,因为 Eralng 毕竟只有少数的几种内置类型,这样一用,立马上就会觉得其类型系统不够强大,表达起来不能自如(但别忘了,函数本身也是一个可用的类型系统)。此乃后话,我们暂且不表。
我们再来说“缺少”的“编译时”的检查环节,其实 Erlang 已经提供了 xref (交叉引用)工具,用来对模块之间调用的“函数名称和参数个数”进行检查,我此前在《Erlang 引用检查小脚本》一文中已经有所提及。
然而,值得注意的是,这个工具并没有被默认包含在“编译阶段”之内,这是一个相当有趣的(而且似乎是不严格和不好的)设计。实际上,我们知道在编译阶段进行的“函数名称和参数个数”检查,并不能完全覆盖所有的情况。比如,在 Java/.NET 中通过反射动态的调用函数,就无法在编译阶段被很好的检查(自动重构自然也就无从谈起)。在 Erlang 中,除了动态调用,还存在大量对函数变量的使用,使得这种情况更为突出。比如 lists 包中大量用到作为参数的函数,等。既然很多情形都不能在编译阶段被检查出来,那么编译器就不徒劳的来做这件事(显而易见的模块内函数调用的检测则不在此列),这或许是 Erlang 编译器在处理这个问题时的逻辑。
这其实是一种设计决策,喜欢与否是见仁见智的。有人会认为这是“不严格的检查,不利于重构”,就会有人会认为是“去掉了不必要的约束,更加有利于项目展开”(每一个模块都应该是内聚的和低耦合的,应该尽量减少对其他模块的编译依赖。请设想,在接口尚未稳定之前,要如何保证 commit 的代码能够总是“通过编译”呢?)。我觉得,这些都是在项目开展过程之中自然都会碰到的两难问题,没有必要去责怪语言。
谈到重构,我们前面已经提到,业务需求在变,重构在所难免,任何语言概莫能外。修改接口带来的一大堆脏活累活总得有人去干,自动重构的工具能够降低这种痛苦,但同样无法避免。比如在 Java/.NET 中,你为接口增加了一个参数,自动重构能自动补上,但,仍然需要为每一处修改其找到值的出处,一连串的代码修改势难避免,而且无法自动完成。在 Erlang 中 xref 则只能帮你找到不符之处,你需要自己从头去改,问题是,这两者之间的区别能有多大?
------------------------------
我并不认为 Erlang 是惟我独尊万夫莫敌,它固有其软肋(比如,密集运算,前端,字符处理,等等等等),明智之人必然会选择合适的技术来解决合适的问题。但 Erlang 在它擅长的领域(主要是客户-服务器模式中的服务端)确实是一个有着自己特点的技术,它有着与众不同的语法特点和与之相适应的思维方式。如果我们处处都以之前积累的经验来判断,必然都会因为“习惯冲突”而产生疑问(比如,按照面向对象的角度,按照设计模式的角度,按照同步调用的角度,等等),这是再正常没有的了。但我们不妨先别急着做出结论,先留一个疑问在这里,继续前行。等过了一段时间,我们回头再看,说不定就会发现,那个疑问其实已经在另外一处得到了解答。
》Joe的论文我只看了两遍,在我看来,Joe论文的精髓是:
》1、并行的任务应该尽量sharing nothing
》2、异步消息传递是并行任务间最合适的协作方式。
》其余的,包括容错性都可以从这两条中推出来
咱们的理解还真不一样,我的理解是Joe论文的精髓就是论文的标题, 软件容错是最根本的,你说的1和2都是从这个里面推出来的,甚至热补丁、伸缩、动态升级都可以从软件容错推出来。
有兴趣的话,可以看看这个:
http://www.cs.chalmers.se/Cs/Grundutb/Kurser/ppxt/HT2007/general/languages/armstrong-erlang_history.pdf
或者看看joe armstrong blog上关于容错的文章。
》这是Erlang能适用的应用场景,但世界上的应用场景是复杂多样的,很多时候我就
》需要简单地搞定优先级问题,比如在我的另一篇blog中谈到的 Erlang中log进程是
》个singleton process时出现的问题(这个问题Ulf也被搞糊涂过,在mail list上
》提问),还好,我算是比较快定位了问题所在,要不然就狠很地影响到我们的系统的
》交付了。
这并不是Erlang的应用场景,这是个和语言无关的设计问题,只是Erlang更推崇这种设计哲学。您定位出来问题后,就是通过调整优先级来解决的吗?如果是这样,只能说您打了个不怎样的补丁,这种问题完全可以通过好的设计来避免,这是个逻辑问题。
》“并发粒度的设计还是得根据问题以及应用场景来定”,开始复杂了吧。在这些场
》景下Erlang的简单性既然没了,我当然选其它的方案。
呵呵,这个怎么就表明Erlang的简单性没有了。
》Erlang就是这么处理的,而且在这一点上正是它的强项呀?
Erlang是怎么处理的?
》“小消息,大计算”:这是指什么?
参见 Joe Armstrong的Programming Erlang最后一章。
不明真相围观群众正在学习中……
@jackyz
》这个逻辑只是被改变成了:“判断是否与函数的某个子句声明相符”
对,推到极限,Joe老头甚至在建议:
形如:
string:substring( string:S start:I length:J)
的句子,应该自动改写成:
string:substring_start_string_length(S, I, J)
从最近Erlang语法层次的讨论我们看到了什么?我们看到了,试图在语法层次引入静态类型,包括打算增加is_type作为guard函数。
》其实 Erlang 已经提供了 xref (交叉引用)工具,
》用来对模块之间调用的“函数名称和参数个数”进行检查
这些都是目前试图解决这些问题的工具。新版的Erlybird中,我也加入了交叉引用检查,如果函数名和参数个数不符,会自动标上下划波浪线。但这些都不是彻底的,或者说不是完全可靠的。
在开发企业级应用时,我非常需要了解我写(或改写)的每一个句子的可靠性,完整可靠的静态类型检查就非常重要了。
另外,类型本身是成系统的,并且,完善、强大的类型系统可以帮助设计出好的应用,Scala的类型系统自恰且有坚实的理论基础,虽然略显复杂但我很喜欢它的逻辑一致性。
@dsun
》这并不是Erlang的应用场景,这是个和语言无关的设计问题
请看清楚我的表述:这是Erlang**能**适用的应用场景。
》》》这个我不赞成,如果单个任务的处理时间(代价)约等于或小于 process/thread的切换时间,
》》》那么process/thread来进行处理就是个错误的设计,不管用啥语言
》》Erlang就是这么处理的,而且在这一点上正是它的强项呀?
》Erlang是怎么处理的?
Erlang就是用便宜的process来处理短信息的,为什么Erlang的process便宜?因为share nothing, 因为创建和切换的代价小呀。用Erlang开发的电信交换机不就是这么处理的吗?
@Caoyuan
》这些都是目前试图解决这些问题的工具。新版的Erlybird中,我也加入了交叉引用检查,如果函数名和参数个数不符,会自动标上下划波浪线。但这些都不是彻底的,或者说不是完全可靠的。
问题是,并不存在完全可靠的设施,你要从“最佳实践”的层面来规范你所使用的接口设施。
比如,你在 Java 里用反射来做调用,同样无法保证你的编译检查是彻底的。正确的做法是用函数来做接口设施(这样才可以得到检查)。这并不是语言支不支持的问题,而是开发的约定和规范。
在 Erlang 里面,用来做接口设施的应该是函数,而且,最好一个函数就干一件事,不要有一大堆子句了(除非是接受两种差异不大的数据类型),这是从可读性上做要求(同样,也只有这样才可以得到检查)。这同样是开发的约定和规范。
》在开发企业级应用时,我非常需要了解我写(或改写)的每一个句子的可靠性,完整可靠的静态类型检查就非常重要了。
了解每一个句子的可靠性,这件事我认为最终应该靠 unit test 来做保证,而不是类型系统。
》另外,类型本身是成系统的,并且,完善、强大的类型系统可以帮助设计出好的应用,Scala的类型系统自恰且有坚实的理论基础,虽然略显复杂但我很喜欢它的逻辑一致性。
这件事就是个人喜好了,我不做评论。
》请看清楚我的表述:这是Erlang**能**适用的应用场景。
我的意思是,通过优先级来影响程序的逻辑,不是个好的方法,和用不用Erlang无关。
》Erlang就是用便宜的process来处理短信息的,为什么Erlang的process便宜?因为
》share nothing, 因为创建和切换的代价小呀。用Erlang开发的电信交换机不就是这
》么处理的吗?
Erlang的process便宜并不见得就得用来处理短消息。我刚刚做过H248的信令处理的性能测试,系统是用Elang做的,每个transaction启动一个process,信令的处理时间可不短,基本上处理性能(每秒处理的transaction个数)和CPU的个数成线性关系。如果是信令处理时间很短,就没这个效果了,根本利用不了多核的处理能力,当然,如果用大规模并发来处理短消息是为了控制流程上的简单,则可以这样用,如果是为了提升性能,就设计错了
另外,share nothing是为了解决容错这个根本问题。
很多“为什么不这样,为什么不那样”,都是因为没有碰到过真正困难问题的拷问。
> But I have one more question: except that this would be much of work to
> “inject” the type system into Erlang, what do you think – are there any
> real obstacles in the nature and the architecture of Erlang/OTP for some
> kind of static typing?
> It seems to me that if it possible we would became a language very
> different from the “original” Erlang.
There are some original design decisions that make retrofitting a
static type system difficult. Two of the big ones are dynamic code
loading and the message passing, but there are most likely lots
of small snags where functions would have been designed with
different semantics (and different type signatures!) if dynamic
typing weren’t available. This is of course partly the “much work”
argument again, but the combination of dynamic typing and advanced
pattern matching is a core characteristic of Erlang, which, for one
thing, leads people to happily write code that would require a PhD
on type systems to even compile in e.g. ML or Haskell. Many would
argue that this is not necessarily a good thing – that static typing
enforces a type discipline that is ultimately good for you.
The truth of that possibly varies depending on problem domain.
Anyway, since we have both Concurrent ML and Concurrent Haskell,
I don’t see that making Erlang statically typed is a high priority.
Concurrent ML was designed at roughly the same time as Erlang.
While Erlang’s main requirement was to support programming of
robust telecoms systems, a main challenge with Concurrent ML was
to add concurrency in a way that didn’t break the type system.
This lead to some fundamentally different design decisions, as
I understand it.
BR,
Ulf W
share nothing相对immutable(readonly)的优势是什么呢?
@Jeffrey Zhao
immutable是语言层面的问题,share nothing是架构层面的问题
我不知道回复谁了,就没有引用的说一个东西。
其实,Actor之间如何调度,与操作系统的thread切换并没有必然联系。并不是说,一个线程在执行了某个Actor的一个任务之后,就一定要进行强制的上下文切换。它完全可以在执行完毕之后,重新取另外一个Actor(同一个Actor,不同Actor都可以)的Mailbox里的任务来执行。所以“大量短任务”并不一定代表会引起问题,还要看怎么实现的。
当然,如果是一个长任务,可能执行的时候已经进行了n次切换了。如果是高级语言实现的Actor模型,应该不会强制进行线程切换。我以前实现过简单的Actor模型和SEDA,这些都是必须考虑的东西。不过我目前还不清楚Erlang和Scala在这方面是怎么处理的,前者是相对低级的VM实现,后者则是高级平台上的框架实现。
我不是指这个。在如.NET,Java在实现Actor模型的时候,很多时候都是通过传递引用(地址)的方式来进行消息交换。这么做也就是共享对象的实例,但是这些实例是immutable的,只读的,也不会影响并发。
而Erlang,据我所知,任何传递都是完全拷贝,我认为这方面会带来额外的开销。这是事实吗?为什么Erlang会设计成这样呢?
@jackyz
》在 Erlang 里面,用来做接口设施的应该是函数,而且,最好一个函数就干一件事,
》不要有一大堆子句了(除非是接受两种差异不大的数据类型),这是从可读性上做要求
》(同样,也只有这样才可以得到检查)。这同样是开发的约定和规范。
在Erlang里约定是这样表达的:
-spec sub_string(string(), pos_integer(), pos_integer()) -> string().
sub_string(String, Start, Stop) -> …….. end.
在Scala里约定是这样表达的:
def sub_string(string:String, start:Int, stop:Int) {
….
}
在Erlang中spec是可选的,当然,也可以写成:
sub_string(String, Start, Stop) when is_list(String), is_integer(Start), is_integer(Stop) -> … end.
这时主要是用来在运行时检查/分发
这里的问题是:
1、在Erlang中,guard函数最初的目的是简单的运行时类型检查和分发约定,-spec, -type是试图加上编译时类型检查;两种机制现在没发合并到一块,或者说,相加上这些时有点晚了,因此有人提出应该写成这样:
sub_string(String::list(), Start::integer(), Stop::integer()) -> … end.
这样就统一了,当然这样也就跟Scala等一样了。
2、即便是有这些约定了,表达更复杂的类型时怎么办?现在Erlang定义复杂类型的主要手段是基本类型加List、Tuple化扩展。但是,这未必是够的。有人可能认为这样就够了,那也没关系,至少,我处理的问题域中,Scala的完整、一致、包括协变的类型体系很有帮助,它可以在很大程度上帮我分析、设计整个系统。
》了解每一个句子的可靠性,这件事我认为最终应该靠 unit test 来做保证,
》而不是类型系统。
unit test,类型系统,各司其职,有什么不好吗?
》如果是信令处理时间很短,就没这个效果了,根本利用不了多核的处理能力,
》当然,如果用大规模并发来处理短消息是为了控制流程上的简单,
》则可以这样用,如果是为了提升性能,就设计错了
process/actor模型本身就是为了让设计和流程控制简单,提升性能倒是有各种各样的办法。好的设计要兼顾简单和性能,actor/process模型对于并发/并行就是一种这样的好的设计。怎么反倒成了设计错了。