近来在博客上关于 Scala 与 Erlang 之间的争论越来越热。未来注定是多核的世界,问题在于如何去解决多核的危机。Scala 和Erlang 是两门渴望成为其解决方案的语言,但它们也有些许的不同。那么它们所采取的方式各有什么利弊呢?
引入问题
摩尔定律已经改变了。我们不再获得和以前一样的时钟频率增长,取而代之,我们得到了更多的(CPU)核心。今天,也许你的笔记本都已经是双核的了。
为了利用多核的特性,应用程序需要支持并发。如果你的客户买了一台八核的机器,要向他们解释清楚正常情况下应用程序只会用到12% 的CPU 能力将是一件费时费力的事,即便该机器是为该应用量身定制的。
在未来,情况可能变得更糟。你的顺序执行代码不仅不会跑得更快,甚至有可能实际上跑得更慢。其原因在于,你获得越多的核心,由于功耗和散热的影响,每个核心就会更慢。几年之后,英特尔就会给我们带来32 核的CPU ,按这种趋势,在我们不知不觉之中数千核的CPU 就会出现。但每个核心都会比现在的核心慢很多。
并发代码
一个显见的解决途径就是编写(或重写)软件以支持并发。最常见的方式莫过于使用线程,但大多数开发者都认为基于线程的应用编写起来特别的困难。死锁,饿死以及竞争条件对大多数并发开发者来说都是太熟悉的概念了。Erlang 和Scala 都大大减轻了这种痛苦。
语言概览
Scala 常被看作是下一个主要的JVM 语言。它结合了面向对象编程的范式和函数式编程的范式,与Java 相比有着更简洁的语法,它是静态类型的,有着跟Java 一样或者有时候更快的运行速度。有太多的理由值得去认真探索一下Scala。
Erlang 是一门为健壮性而生的语言,由于它的设计,它自然又是一门有着极好伸缩性的语言。它比Java 历史更早,但却常被看作引领未来并发的语言。它是一门动态类型的函数式语言,有着一些非凡的系统正常运行时间的成功例子。
争论核心
那么Scala 与Erlang 争论的到底是什么呢?说到底,就是性能表现与伸缩能力,但这争论也包括了其它像风格,语言特性以及库支持等等。这场争论开始于 Ted Neward 有一次无心的给出了他对几种语言的看法,并称“[Erlang] 在其自己解释器上运行事实上[是] 很差的”。
Steve Vinoski 与 Ted 于是展开了几轮争论,但这一讨论很快转移到了更多的博客上,争论的焦点也转变成了 Scala 与 Erlang 之间那些有趣的差异和共同点。我们将总结每个有意思的论点并给出每种语言的利弊,并表达对一些问题的各种不同看法。
可靠性
Steve Vinoski 就 Ted 所发表的帖子进行了回应,给出了他对于“Erlang 在其自己解释器上运行”的感受:
事实上,Erlang 在其自己解释器上运行得,很好很强大;如若不然,不可能有如此好的可靠性,它将只是又一个面向并发的有趣但却无用的语言实验而已。
Steve 谈到这个问题,就算一个语言本身可靠,它所依赖的基础也必须可靠才行。因为 Erlang 从骨子里就是为可靠性而设计的,从而支持并发,所以它不会受到一些并发性常见问题的影响,这主要是底层库包在并发环境下运行很好。
另一方面,Scala 是站在 JVM 之上的,所以一个重要卖点在于可潜在地使用所有现成的 Java 代码。然而,大部分的 Java 代码并非专为并发而设计的,使用 Scala 代码时要将此考虑进去。
轻量级进程
要运行大规模并发应用,你需要大量的并行执行。这可以通过几种方式达到。使用线程是一种常见的方式,使用进程又是另一种。其区别之处在于,线程与其它线程之间共享内存,而进程是相互独立的。这意味着线程需要像互斥信号这样的锁机制,防止两个线程在同一时间对同一内存进行操作,但进程不会受此问题影响,相反是使用一些消息传递机制来跟其它的进程间通信。但进程在性能和内存方面的代价是昂贵的,这正是人们就算知道基于线程的并发是一种极复杂的编程模型也宁愿选择它的原因。
Steve Vinoski 这样写到:
提供互不共享的轻量级进程架构将使得大规模并发能力变得十分容易,但这并不意味着一旦你设计好了,剩下的就只是编程的工作那么简单。
Erlang 采取了这样的并发方式。一个 Erlang 进程是非常轻量化的,Erlang 应用常常拥有成千上万的线程甚至更多。
Scala 通过基于事件的 Actor 从另一方面达到了同样的效果。 Yariv Sadan 解释说:
Scala 有两种类型的 Actor:基于线程或是基于事件。基于线程的 Actor 在重量级的 OS 线程内部执行。它们从不相互阻塞,但每个 VM 上可伸缩的 Actor 不会多于几千个。基于事件的 Actor 是简单的对象。它们是十分轻量化的,并且,像 Erlang 进程一样,因此它们可以在一台现代的机器上数以百万计的产生。
Yariv 解释到,尽管如此,这里面也还是有一些区别的:
与 Erlang 进程的区别之处在于,每个 OS 线程内部,基于事件的 Actor 是顺序执行的并且使用没有强占式调度算法。这使得一个基于事件的 Actor 可能在很长一段时间内阻塞其 OS 线程(甚至是无限的)。
不可变性
Erlang 是一门函数式语言。这意味着其数据是不可变的,像 Java 的 String 一样,并且没有副作用带来的风险。对数据的任意操作会产生一个该数据新的修改后的版本,但原数据仍然不变。在谈到健壮性的时候,不可变性是其需要高度注意的一个因素,因为没有代码可以无意间修改其它代码依赖的数据,但从并发的观点来看,不可变性也是一个十分重要的特性。如果数据不可变,其被两个并行执行路径更改的风险就不存在,因为没有办法改变它且不需要保持同步,所以数据可以被拷贝到其它机器上。
因为 Scala 构建在 JVM 之上,结合了面向对象和函数式方法的特点,它不具备像纯函数式语言的不可变性的保证。然而,在 Yariv 日志的评论部分,Yariv 和 David Pollack 就这两门语言之间的差别展开了一场有趣的讨论。David, Scala Web 框架 Lift 的作者,给出了他对于不可变性的看法:
不可变性 —— Erlang 强制了这一点,而且你几乎无法绕过它。但是,与强制一个单一类型相比,你可以用 Scala 神奇强大的类型系统的剩余部分去交换。我在进行 Scala Actor 编码时使用不可变数据,而让 Scala 的类型系统负责其它类型。
Yariv 问到:
只发送不可变类型难道不是一个重大限制吗?这意味着,例如,你不能从 Hibernate 装载一个简单的 bean 并将它发送给其它 Actor。
David 回答到:
我曾基于 Scala 的 Actor 构建个多个生产系统。实际上对于不可变性问题并没有多少工作需要处理。你只需要将你的相关类(消息)定义为不可变的,其它的就不用管了。
类型系统
Erlang 是动态类型的,而 Scala 是静态类型的并且相比 Java 有着更强的类型系统。然而,与 Java 相比最大的一个区别是 Scala 可以类型推断。这意味着你可以省掉大部分的类型注解,使得代码更加干净而编译器照样会做所有的检查。
关于动态与静态系统之间孰是孰非的争论看来永远也不会停止,但 Erlang 和 Scala 之间却有着显而易见的区别。
尾递归或是循环
Yariv 又提到:
函数式编程与递归从来都是形影不离的。实际上离开了尾递归你很难写出有用的 Erlang 程序,那是因为 Erlang 没有循环——它对一切都使用递归(这在我看来是一件好事 :))。
这显然使得 Erlang 与 Scala 产生了很大差别。Scala 提供了很多更传统的迭代,但 David Pollack 并没看出在这种环境下尾递归有什么优势:
尾递归——对基于事件的 Actor 来说根本不是什么问题。
如此说来,这仅仅有关你的偏爱和风格罢了。
热交换代码
由于 Erlang 是为可靠性而生的,热交换代码(运行时替换代码)是其内建的天性。
JVM 对热交换代码有所支持。类可以被改变,但由于其静态的类型系统,其方法签名是不可改变的——只有其内容可以改变。虽然有第三方工具致力于此,也有框架(提倡以一种使运行时更方便交换类的编程风格书写代码),但就算运行在JVM 上,如何进行交换仍是取决于你的Scala Actor 是如何构建的。Jonas Bonér 就此给出了一个详尽的例子。
总结
Scala 和 Erlang 都是致力于解决多核危机的语言。它们来自不同的背景和年代,因此对待某些问题的方式也不尽相同,然而在许多方面它们的共识大于分歧,至少在并发性上如此。
Erlang 已经有着数十年的历史,并且已经在许多关键的真实系统中证明了自己。其不足之处在于它有一点像一个孤岛,最近的多语言编程的趋势似乎对Erlang 社区影响不大。
另一方面,Scala 是同一类型应用的新生儿。一些真实应用即将诞生,并且一些公司将未来押在了上面。Scala 相对Erlang 的最大优势在于,它运行在JVM 之上并且可以利用所有现成Java 代码、框架和许多工具。话虽如此,这种强大的能力也要求了更大的责任,因为大部分Java 代码不可能自动适应Scala 的Actor 模型。
对于主流语言无法帮开发者解决的压力越来越大的问题,两种语言都对提供了相似的解决途径。希望你在读完这篇争论总结之后,能更清楚哪种语言更适合你的特殊要求,并对其深入了解。
未来是多核的。Scala 和Erlang 将会越来越流行。
评论