编程语言之Erlang
2010-12-15 15:49:01 阿炯

Erlang是一个结构化,动态类型编程语言,内建并行计算支持。最初是由爱立信专门为通信应用设计的,比如控制交换机或者变换协议等,因此非常适合于构建分布式,实时软并行计算系统。它是一种面向并发(Concurrency Oriented),面向消息(Message Oriented)的函数式(Functional)编程语言。从OTP 18.0开始使用Apache许可证2.0。

使用Erlang编写出的应用运行时通常由成千上万个轻量级进程组成,并通过消息传递相互通讯。进程间上下文切换对于Erlang来说仅仅只是一两个环节,比起C程序的线程切换要高效得多得多了。使用Erlang来编写分布式应用要简单的多,因为它的分布式机制是透明的:对于程序来说并不知道自己是在分布式运行。Erlang运行时环境是一个虚拟机,有点像Java虚拟机,这样代码一经编译,同样可以随处运行,它的运行时系统甚至允许代码在不被中断 的情况下更新。另外如果你需要更高效的话,字节代码也可以编译成本地代码运行。

Erlang得名於丹麦数学家及统计学家Agner Krarup Erlang,同时Erlang还可以表示Ericsson Language。Erlang语言由瑞典爱立信电信公司的Joe Armstrong开始设计,开始于上世纪80年代。最初是以Prolog程式语言为基础,几度改版之后,改成以Joe's Abstract Machine为基础的独立语言执行环境。虽然语言风格仍与Prolog相近,不过因Erlang语言设计的走向,Erlang成为具被函数语言特色的程式语言。


版本历史
1998年起,Erlang发布开放源码版本,称为开源电信平台。开源电信平台采用修改过的Mozilla公共许可证协议发放,同时爱立信仍然提供商业版本的技术支持。目前,Erlang最大的商业用户是爱立信,其他知名用户有北电网路、亚玛逊以及T-Mobile等。

Erlang 最初是爱立信为开发电信相关产品而产生,即开放电信平台(Open Telecom Platform)的缩写,Erlang 开源前这个名字多少还有点品牌效应。无论 Erlang 还是 OTP 都早已不再局限于电信应用:更贴切的名字应该是“并发系统平台”;这个命名的由来可能跟 Erlang 最初服务的业务相关,毕竟 Erlang 曾经是通信行业巨头爱立信所有的私有软件。实际上后来 OTP 的设计和功能已经脱离了它名称的本意,所以 OTP 应该被看作一种名意无关的概念。OTP是一个用 Erlang 编写的应用服务器,它是一套 Erlang 库,由 Erlang 运行时系统、主要使用 Erlang 编写的许多随时可用的组件以及 Erlang 程序的一组设计原则组成。

与大多数程序以及编程语言相反,OTP 应用本身不具备一个阻塞程序执行的主执行流(线程/进程之类的并行单元),准确的说是 OTP 应用自身的进程并不阻塞应用,Erlang 的面向进程编程便是这个的前提。对于 OTP 应用而言,应用本身是由多个进程组成的,一般来讲是一种监督树结构,这些进程会出现不同的分工但不会具备任何特权。与之相对的,例如常规程序是由一个启动应用的线程阻塞来维持运行的,如果这个线程结束了那么程序就结束了(通常所有的后台线程会被释放)。但是 OTP 应用是由 ERTS(Erlang 运行时系统) 来加载启动的,每一个进程都是平等的,会发现其实每一个 OTP 应用都类似于由多个微服务(进程)组成的系统,面向进程编程就是在这个系统上开发出一个个的“微服务”,具备这个原则设计的程序便是 OTP 应用。


Erlang 应用场景与设计理念

分布式产品,网络服务器,客户端,等各种应用环境。
Erlang 也可以作为一种快速开发语言,进行原型开发。
应用需要处理大量并发活动。
需要良好的软件或硬件容错(fault-tolerant)能力。
软件产品需要在多服务器中具有良好的伸缩能力,而不必大改动。
容易实现不中断服务进行升级过程,软件需要在严格的时间片响应用户,比如游戏服务器。

通过定下的九条原则性思想设计,写出来天然支持分布式系统的 Erlang 以及 OTP 框架,真的做到了面向并发:
一切皆进程
进程强隔离
进程的生成与销毁都是轻量的操作
消息传递是进程交互的唯一方式
每个进程有唯一的名字
你若知道进程的名字,就可以向他发消息
进程之间不共享资源
错误处理非本地化
进程要么正常跑着,要么马上挂掉

就以上九条的观念,设计出的 Erlang 语言,成就了可靠性达到 99.9999999% 的目前世界上最复杂的 ATM 交换机。


语言特色
并行程序设计 在语言中,可以藉由spawn/*函数,将特定的函数设定为独立的进程,之后可以做跨进程通讯。
函数式程序设计 由于Erlang早期以Prolog开发制成,受语言特性影响,即成为函数式语言。
单次赋值 每个变量只能跟数据绑一次,所以,不像一般程序设计语言的变量可以多次指定为不同的值。单次赋值的好处是状态单纯,使程序容易阅读。
及早求值或严格求值 Erlang基本求值策略为电脑语言中及早求值之特性。而且,可以藉由明确使用无参数的λ表达式,将特定函数设定为惰性求值策略。
动态数据类型与类型系统 有编译时期的类型检查系统支持。
快速失败 在执行时期发生的错误,会由错误位置送出讯息,发生错误的进程立刻停止执行。藉由进程通讯机制,可以自动传递错误、捕捉错误,使其他进程能够帮助处理错误。
代码热更新 由于Erlang是函数语言,可以撰写特定的程序结构,制作即时更换新版函数的机制。
脚本语言 Erlang实作提供了脚本执行方式。


Erlang程式结构以函数定义为主。函数是一组将输入分别对应到输出的规则,对应方式遵守数学函数的惯例。此外,Erlang语言由几项构句要素所组成,包括文字(或称原子)、数字、列表、值组、字元、字串、二进位资料、模组、与特定用途的关键字如fun ... end, if ... end, case ... of ... end, spawn, !, receive ... end等等。以下段落分别列示并举例说明Erlang程式的基本构成部份,涵盖资料格式、表达式格式与内建函数。

语言特性

简单小巧
Erlang 简单小巧只有 6 种基本的数据类型,另外提供几种复合结构,这就是 Erlang 的所有数据类型。
Atom
bitstring
Number (float, integer)
List
Maps
Tuple
Reference
Fun
Port
Pid
String
Record
Boolean

在 Erlang 中表示任何类型的数据都叫做 Terms,它是源代码中的基本数据类型。而常见的 string 在 Erlang 中是以位串 bitstring 或 List 表达的,没有 Boolean 类型,使用 atoms 原子类型的 true & false 替代。

模式匹配
在 Erlang 的函数中,= 号不是赋值,而是模式匹配,某些语法中,如 C# 8.0 也可以使用 Pattern 匹配,这是一个非常好的特性,我们可以让代码自己去决定如何执行。比如,我们定义一个函数,其告诉我们某种水果的价格:
price(apple) -> 2.0;
price(banana) -> 1.2.

随后调用 price(Fruit),会根据 Fruit 变量的内容返回具体的价格。这样做的好处就是节省了代码量,不用 if...else… 或者 switch…case 的来伺候了。也便于代码的扩展:加一个新的水果品种,只需要加一行就可以了。学习 Erlang 一个非常重要的内容就是模式匹配,但是请不要混淆,这个匹配和正则表达式没有任何关系。

变量单次赋值
一个匪夷所思的特性,变量竟然只能单次赋值!是的 Erlang 中变量一旦绑定某个数值以后,就不能再次绑定,这样做的好处是便于调试出错,更深层次的原因是 Erlang 为并发设计,如果变量可以修改,那么就涉及到资源的加锁解锁等问题,当发生错误时,某个变量是什么就永远是什么,不用顺藤摸瓜的查找谁修改过它,省了好多事情。唯一的麻烦就是需要一个信的变量时,你必须再为它想一个名字。

提供了丰富的 libs
stdlib 中包含大量的数据结构如 lists,array,dict,gb_sets,gb_trees,ets,dets 等
mnesia 提供一个分布式的数据库系统
inets 提供 ftp client,http client/server,tftp client/server
crypto 提供加密解密相关函数,基于 openssl 相关实现
ssl 实现加密socket通信,基于openssl实现
ssh 实现ssh协议
xmerl 实现XML相关解析
snmp 实现SNMP协议(Simple Network Management Protocol)
observer 用来分析与追踪分布式应用
odbc 使 Erlang 可以连接基于SQL的数据库
orber 实现 CORBA 对象请求代理服务
os_mon 提供对操作系统的监控功能
dialyzer 提供一个静态的代码或程序分析工具
edoc 依据源文件生成文档
gs 可以为我们提供某些 GUI 的功能(基于Tcl/Tk)

灵活多样的错误处理
Erlang 最初为电信产品的开发,这样的目的,决定了其对错误处理的严格要求。Erlang 中提供一般语言所提供的 exception,catch,try…catch 等语法,同时 Erlang 支持 Link 和 Monitor 两种机制,我们可以将 Process 连接起来,让他们组成一个整体,某个 Process 出错,或退出时,其他 Process 都具有得知其推出的能力。而 Monitor 顾名思义,可以用来监控某个 Process,判断其是否退出或出错。所有的这些 Erlang 都提供内在支持,我们快速的开发坚固的产品,不再是奢望。

代码热替换
你的产品想不间断的更新么?Erlang 可以满足你这个需求,Erlang 会在运行时自动将旧的模块进行替换。一切都静悄悄。

天生的分布式
Erlang 天生适合分布式应用开发,其很多的 BIF 内建函数都具有分布式版本,我们可以通过 BIF 在远程机器上创建 Process,可以向远程机器上的某个 Process 发送消息。在分布式应用的开发中,我们可以像 C、C++,Java 等语言一样,通过 Socket 进行通讯,也可以使用 Erlang 内嵌的基于 Cookie 的分布式架构,进行开发。当然也可以两者混合。分布式开发更加方便,快速。Erlang 的 Process 的操作,Error 的处理等都对支持分布式操作。

超强的并发性
由于采用其自身 Process,而没有采用操作系统的进程和线程,我们可以创建大规模的并发处理,同时还简化了我们的编程复杂度。我们可以通过几十行代码实现一个并发的TCP服务器,这在其他语言中都想都不敢想!

多核支持
Erlang让您的应用支持多个处理器,您不需要为不同的硬件系统做不同的开发。采用Erlang将最大限度的发挥你的机器性能。

跨平台
如同JAVA一样,Erlang 支持跨平台(其目前支持linux,mac,windows等19种平台),不用为代码的移植而头疼。仅仅需要了解平台的一些特性,对运行时进行优化。

开源
开源是我非常喜欢的一个词汇,开源意味这更加强壮,更加公开,更加的追求平等。开源会让 Erlang 更好。

Erlang的分布式编程

1、什么是分布式程序?
Erlang分布式程序是设计用来在网络计算机上运行并且只可以通过消息传递来协调活动

2、为什么要写分布式程序?
1)性能
通过将程序的不同部分分布到不同计算机来并行运行,以此提升性能(适用于密集型、CPU是瓶颈的计算)
2)可靠性
通过将程序分布来构建容错系统,以此提升可靠性。如果一台机器失败了,我们可以在另一台机器上继续计算
3)伸缩性
如果我们scale up,迟早有一天机器性能会成为瓶颈。增加新机器应该是一个很简单的操作并且不需要应用程序架构做太大改动
4)本质上分布的程序
多用户游戏或聊天系统等程序的在本质上就是分布的,如果我们有大量在地理位置上不同的用户,我们希望将计算资源放在离用户近的地方
5)乐趣
大部分有意思的程序都是分布式的,这些程序大多数需要和全世界的人和机器交互

3、Erlang里两种分布模型
1)分布式Erlang
程序运行在Erlang节点上,可以在任一节点上spawn一个进程,任何节点都可以在其他节点上做任何操作,所有的消息传递和错误处理就和在一个单一节点上运行一样
2)基于Socket的分布
使用TCP/IP socket来分布,没有上面的分布式Erlang强大

4、erl -sname freeoa表示在本地启动一个Erlang节点,名字为freeoa
如果两个节点在同一机器上,或者没有DNS服务,只能用-sname参数,s表示short
不同的机器上使用-name参数来启动节点

5、rpc:call(Node, Mod, Func, [Arg1, Arg2, ..., ArgN)在Node上执行一个远程过程调用,要调用的方法是Mod:Func(Arg1, Arg2, ..., ArgN)

6、使用-setcookie参数来保证交互的两个节点有一样的cookie,以此保证安全性

7、使用-kernel inet_dist_listen_min Min \ inet_dist_listen_max Max参数来设置端口范围

8、分布式程序用到的BIF:
1) @spec spawn(Node, Fun) -> Pid
2) @spec spawn(Node, Mod, Func, ArgList) -> Pid
3) @spec spawn_link(Node, Fun) -> Pid
4) @spec spawn_link(Node, Mod, Func, ArgList) -> Pid
5) @spec disconnect_node(Node) -> bool() | ignored
6) @spec monitor_node(Node, Flag) -> true
7) @spec node() -> Node
8) @spec node(Arg) -> Node
9) @spec nodes() -> [Node]
10) @spec is_alive() -> bool()

9、分布式程序库:
1)rpc
2)global
rpc module里最常用的方法是call(Node, Mod, Function, Args) -> Result | {badrpc, Reason}

10、设置cookie的三种方式
1)$HOME/.erlang.cookie
2)erl -setcookie C
3)erlang:set_cookie(node(), C)

Erlang为什么高效率

平台:Linux 平台切换进程的速度很快,而在 Windows 平台就很慢了,所以 Windows 鼓励使用线程,Linux 鼓励使用进程。而 Erlang 是自己实现的进程,不局限于任何平台,切换起来是飞快的。

使用者:Erlang 小众,使用它的都是工程师,所以代码效率高。任何语言刚开发出来,都在学院使用,然后是工程师,等到普及以后,各种问题就随之而来,比如 java。

Erlang 中的进程是它自己实现的用户级进程,这里不是指操作系统级的进程/线程。Erlang 进程非常轻量,短时间能快速创建和销毁,并且切换代价小;Erlang 进程间通讯使用消息,而不是多线程编程中常用的锁机制,没有等待锁的时间消耗。erlang是一个操作系统,而不只是语言。这一点非常重要,golang也是如此。erlang自己重新定义了调度单元(erlang进程),实现了调度器,调度开销非常小。erlang是一种新的编程范式,新的思维方式(相对C++ Java Python这些语言而言)。

erlang没有全局变量、而且变量不可变、进程间靠消息通信…… 这些语言上的限制和要求,使得在设计系统的时候只能采用erlang的思维方式,否则写不出代码,没法实现。最终得到的就是天然避免或减少了数据共享的并发性很高的系统。——共享就会有竞争,竞争就需要锁,锁就是多核时代并发编程的最大敌人,没了锁性能就能提高很多。erlang是从系统底层开始构建其并发机制的,erlang中的进程其实是linux中kernel space中userspace级别的线程,包括调度基本都是在kernel级别完成的,所以调度起来很高效。erlang并发高效其实相对于应用场景来说的,erlang本身最适合应用于通信方面的应用,本身计算和执行能力还是有限的。

Erlang并发机制之进程调度

Erlang调度器主要完成对Erlang进程的调度,它是Erlang实现软件实时和进程之间公平使用CPU的关键。Erlang运行时,有4种任务需要被调度:进程,Port,Linked-in driver,Erlang虚拟机的系统级活动。

Erlang调度器主要有以下特点:
1. 进程调度运行在用户空间 :Erlang进程不同于操作系统进程,Erlang的进程调度也跟操作系统完全没有关系,是由Erlang虚拟机来完成的;

2. 调度是抢占式的:每一个进程在创建时,都会分配一个固定数目的reduction(R15B中,这个数量默认值是2000),每一次操作(函数调用),reduction就会减少,当这个数量减少到0时或者进程没有匹配的消息时,抢占就会发生(无视优先级);

3. 每个进程公平的使用CPU:每个进程分配相同数量的reduction,可以保证进程可以公平的(不是相等的)使用CPU资源;

4. 调度器保证软实时性:Erlang中的进程有优先级,调度器可以保证在下一次调度发生时,高优先级的进程可以优先得到执行。

SMP支持

从R11B(2006)Erlang开始支持SMP(Symmetrical Multi Processor,也就是多核)。Erlang对SMP的支持分为以下几个阶段:
1). 单调度器、单运行队列:调度器运行在虚拟机主进程中的一个线程中,从单个任务队列中获取运行进程,因为只有一个线程,所以对运行队列的访问不需要锁;

2). 多调度器、单运行队列:调度器的个数可以自定义(参见erl命令的+S参数,默认数量同CPU核的数量),每个调度器运行在一个线程中,但是只有一个运行队列,所有调度器都从同一个运行队列获取运行进程,所以会涉及到共享资源的访问,需要用到锁。

3). 多调度器、多运行队列:每个调度器都绑定有一个运行队列,每个调度器都从各自的运行队列中获取运行进程。相比单运行队列,多运行队列会减少锁冲突,提高性能,但是,因为涉及到多运行队列,就必需要考虑负载问题:如果一个调度器很忙,另一个很闲,那怎么办?Erlang虚拟机存在一个任务迁移的逻辑,来保证各个调度器达到平衡。

进程优先级

Erlang进程有四种优先级:max, high, normal, low(max只在Erlang运行时系统内部使用,普通进程不能使用)。Erlang运行时有两个运行队列对应着max和high优先级的运行任务,normal和low在同一个队列中。

调度器在调度发生时,总是首先查看具体max优先级的进程队列,如果队列中有可以进行的进程,就会运行,直到这个队列为空。然后会对high优先级的进程队列做同样的操作(在SMP环境,因为同时有几个调度器,所以在同一时间,可能会有不同优先级的任务在同时运行;但在同一个调度器中,同一时间,肯定是高优先级的任务优先运行)。

普通进程在创建时,一般是normal优先级。normal和low优先级的进程只有在系统中没有max和high优先级的进程可运行时才会被调度到。通常情况下,normal和low优先级的进程交替执行,low优先级获得CPU资源相对更少(一般情况下):low优先级的任务只有在运行了normal优先级任务特定次数后(在R15B中,这个数字是8)才会被调度到(也就是说只有在调度了8个normal优先级的进程后,low优先级的进程才会被调度到,即使low优先级的进程比normal优先级的进程更早进入调度队列,这种机制可能会引起优先级反转:假如你有成千上万的活动normal进程,而只有几个low优先级进程,那么相比normal进程,low优先级可能会获得更多的CPU资源)。

下图为算法流程图:


参考来源:
Erlang/OTP Doc

Frequently Asked Questions

Erlang十分钟快速入门

Erlang不能错过的盛宴

Erlang OTP 设计原理文档



最新版本:25
Erlang/OTP 25.0 已于2022年5月上旬发布,这是一个新的重要版本,带来了新特性、改进和修复,当然也包含一些不兼容的改动:
stdlib
引入新函数filelib:ensure_path/1,用于确保给定路径的所有目录都存在
为maps模块引入新函数groups_from_list/2和groups_from_list/3
为listsmodule模块引入新函数uniq/1 uniq/2
将新的 PRNG 添加到rand模块,用于快速生成伪随机数

compiler, kernel, stdlib, syntax_tools
增加了对EEP-60中描述的可选择特性的支持。在编译过程中可以用erlc的选项(ordinary and +term) 以及文件中的指令来启用/禁用特性。类似的选项可以用在erl中,用于启用/禁用运行时允许的特性。新的maybe表达式EEP-49作为 may_expr 特性被完全支持。

erts & JIT
JIT 现在适用于 64 位 ARM 处理器
JIT 现在根据 BEAM 文件中的类型信息进行基于类型的优化。
改进了 JIT 对perf和gdb等外部工具的支持,允许它们显示行号,甚至可以找到原始的 Erlang 源代码。

详情查看发行公告

最新版本:26
Erlang/OTP 26.0 已正式于2023年5月中旬发布。这是一个重要版本更新,包含许多新特性、改进和不兼容的变化:

改进 Shell
支持自动补全变量、记录名称、字段名称、map keys、函数参数类型和文件名
支持在 Shell 中打开外部编辑器以编辑当前表达式
支持在 Shell 中定义(包含类型)的记录、函数、规范和类型

采用新的终端:该版本重写了 TTY/终端子系统。Windows 用户会注意到 erl.exe 具有与普通 Unix shell 相同的功能,werl.exe 只是 erl.exe 的符号链接。这使得 Windows Erlang 终端体验与 Unix 保持一致。

优化编译器和 JIT
已优化具有固定大小 segment 的二进制文件的创建和匹配
优化 UTF-8 segment 的创建和匹配
对添加到二进制文件的优化
编译器和 JIT 现在生成更好的代码来创建小型 map,其中所有键都是编译时已知的字面量
基于上述优化,base64 模块的性能有了显著提升。在具有 JIT 的 x86_64 系统上,编码和解码的速度几乎是第25版中的三倍
改进解析工具与更新标准库
改进 Maps、SSL、lists 模块
无需在运行时系统中启用特性 maybe
为 Dialyzer 引入增量模式 (Incremental mode)
引入 argparse -- Erlang 的命令行解析器

详情查看更新说明


官方主页:http://www.erlang.org/