世界是并行的,Erlang程序反应了我们思考和交流的方式,人作为个体通过发送消息进行交流,如果有人死亡,其他人会注意到。
Erlang里的模块类相当于OOPL中的类,进程相当于OOPL里的对象或类实例。
并发编程可以用来提升性能,创建可扩展和容错的系统,以及编写清晰和可理解的程序来控制现实世界里的应用。
并发程序是以一种并发编程语言编写的程序,并发编程语言拥有用于编写并发程序的语言结构。Erlang的并发程序是由互相通信的多组顺序进程组成,一个进程就是一个轻量级的虚拟机,可以执行单个的Erlang函数,只能通过发送和接收消息来与其他进程通信。也就是说,并发性是由Erlang虚拟机提供的,并操作系统的并发控制粒度要小很多。
在Erlang里:
创建和销毁进程是非常快的
在进程间发送消息是非常快的
进程在所有操作系统上都具有相同的行为方式
可以拥有大量的进程
进程间不共享内存,是完全独立
唯一的沟通方式是消息传递,每个进程都有一个邮箱与进程同步创建。
动态代码载入是Erlang特性之一,函数调用的总是最新模块里的最新版函数,哪怕当代码在模块里运行时重新编译了该模块也是如此。
基本元素操作
Erlang shell中,用句号加空格、tab或回车来结束表达式,%表示注释的起点,;隔离子句。模块是.erl 文件,库的头文件.hrl, shell中的编译时c(),外编译命令时erlc, 退出shell用q(),或erlang:halt().
变量以大写字母开头,且不能重新绑定变量,只能一次性赋值,具有不可变状态。原子是全局的,不需要宏定义或包含文件,以小写字母开头,还可放在单引号内,是极简表达式。
元组(tuple)是一些数量固定的项目归组成单一实体{,}, 由于是匿名的,通常在第一个元素上贴标签,来增加可读性。提取元组中的值使用模式匹配操作符=,为匿名变量,多个不必绑定相同的值。但是,_Mode则是常规变量。例如:
2> Family={family,father,mother,son}. {family,father,mother,son}3> {_,X,Y,Z}=Family. {family,father,mother,son}4> X. father5> Y. mother6> Z. Son
记录(record)是元组的另一种形式,可以给元组的各个元素关联一个名称。使用记录的情形:
1) 用一些预先确定且数量固定的原子表示数据
2) 元素数量和元素名称不会随时间改变
3) 大元组中每个元组有相同的结构
采用#myrecord{k1=v1,k2=v2..}来创建record。
列表(list)形如[,,]可以存放任意数量的事物。Head可以是任何事物,Tail通常仍然是个列表。只要用[…|T]构建一个列表,就应确保T是一个列表。同样使用模式匹配来提取列表中的元素。列表推导的常规形式:
[X||Qualifier1,Qualifier2,…]
X是任意一表达式,限定符qualifier可以生成器,位串生成器或过滤器。生成器的写法
Pattern<- ListExpr
反转一个列表时,要调用lists:reverse.
Erlang中没有字符串,字符串是个整数列表,”HelloCloud”是一个列表的简写,io:format来指定打印输出。
映射组(map)是键值对的关联性集合,内部作为有序计划存储,适用情形:
1) 键不能预知时来表示键值对数据结构
2) 存在大量不同键来表示数据
3) 效率不重要时的万能数据结构
4) 自解释型数据结构
5) 用来表示键值解析树,如xml或配置文件
6) 用Json来通信
映射组的语法:
#{key1 op val1,key2 op val2,…,KeyN op valN}
'#后没有名称,op是=>或:=之一。 => 将现有键值更新为新值或给映射组增加一个新键值对。 :=用于更新。 健不能包含任何变量,值可以包含未绑定变量,在模式匹配成功后绑定。
映射组可以通过io:format 里的~p选项输出,并用io:read 或file:consult读取。
Maps:to_json(Map)->Bin 转化为二进制型json
Maps:from_json(bin)-> Nap, 将二进制json转化为map
Json与映射组的对应关系:
1) Json数字:Erlang的整数或浮点数
2) Json字符串:Erlang二进制型
3) Json列表:Erlang列表
4) true和false 对应
5) 映射组中的健必须是原子,字符串或二进制型,值必须用JSON的数据类型表示
模块与模式匹配
模块是Erlang的基本代码单元,erl文件编译后以.beam作为扩展名,采用UTF8字符集,.erl文件示意如下:
-module(模块名,与存放模块的文件名相同) -export([方法名/输入参数的个数]) Method1( {a,b,c})->a*b*c; Mehtod2({d,e})->d-e.
模块属性有两种类型:预定义型和用户定义型。
Erlang中用于代表函数的数据类型被称为fun,相当于Python中的lambda,一般用于
1) 对列表里的每个元素执行相同的操作
2) 创建自己的控制
3) 实现可重入解析代码,解析组合器或者lazy evaluator
模式匹配是Erlang的根基,case和if表达式使Erlang小而一致。
case Expression of Pattern1[ when Guard1] -> Expr-seq1; Pattern2[when Guard2]-> Expr-seq2; …endif Guard1-> Expr_seq1; Guard2-> Expr_seq2; …end
Erlang有两种方法来捕捉异常错误,一种是把抛出异常的调用函数封装在一个try_catch 表达式里,提供了概括信息,另一种是把调用封装在一个catch表达式里,提供了详细的栈跟踪信息。在捕捉到一个异常后,可以调erlang:get_stacktrace()来找到最近的栈信息。
把二进制型,位串,和位级模式匹配引入Erlang是为了简化网络编程。二进制型是置于双小于号和双大于号之间的一列整数或字符串。
例如: 1> Mybin1 = << “I LOVE YOU”>>
Term_to_bingary(Term) ->Bin 转换为二进制型
Binary_to_Term(Bin) ->Term 二进制型转换为Erlang的数据类型
精心选择宏的名称和Erlang代码布局,能最大限度地缩小C和Erlang的语义鸿沟。在Erlang里,最小的寻址单元是1位,位串里的位序列可直接访问。
运行
运行Erlang程序的方式:
1) 在Erlang shell 中编译执行
2) Shell 脚本执行,例
Hello.sh#!/bin/shErl –noshell –pa /home/abel/practice/erlang/code –s hllstart –s init stop
3) 作为Escript 运行,例
#!/usr/bin/env escriptMain(args)-> Io:format(“Hello world ~n”)
内置函数apply能调用某个模块的某个函数并传参。每个erlang进程都有一个被称为进程字典的私有数据存储区。为了增强类型的表达能力,可以用描述性变量给它们加上注解,类型规范为spec,类型说明type。通过dialyzer可以检查程序中的类型错误,最好写模块时先考虑类型并声明它们,然后编写代码。两个载入路径的函数:
-spec code:add_patha(Dir)=>true|{error:bad_directory} 载入路径头加入
-spec code:add_pathz(Dir)=>true|{error:bad_directory} 载入路径尾加入
通过os:cmd(command)可以在erlang中调用shell的脚本,查找标准库源码的命令code:which(file).
Make 是erlang的任务自动化工具,可以通过它来运行程序。下面是一个简单的makefile:
.SUFFIXES: .erl .beam .erl .beam: erlc -W $<ERL = erl –boot start_cleanMODS = module1 module2 module3all: compile $(ERL) –pa ‘home/abel/…/dir’–s module1 startcompile: ${MODS:%=%.beam}clean: rm -rf *.beam erl_crash.dump
如果Erlang程序崩溃了,会留下一个erl_crash.dump文件,可以通过web故障分析器来分析,命令如下:
1> crashdump_viewer:start().
并发
Erlang中基本的并发函数
1) Pid =spwan(Mod,Func,Args) 创建一个新的进程来执行apply(Mod,Func,Args),与调用进程并列运行,会使用最新的代码定义模块。
2) Pid!Message 向Pid进程异步发送Message,!为发送操作符
3) Receive … end 接收消息
receive Pattern1[when Guard1]-> Expression1; Pattern2[whenGuard2]->Expression2; … aftertime-> Expressions end.
内置函数erlang:system_info(process_limit)可找出所允许的最大进程数,默认为262144.
进程注册的内置函数有:
register(AnAtom,Pid)用名称注册Pid
uregister(AnAtom) 注销关联注册
whereis(AnAtom)->Pid|undefined 检查Pid是否注册
registered()->[AnAtom::atom()]返回系统里所有注册进程的列表。
并发程序模板:
-module(ctemplate). -compile(export_all).start() -> Spwan(?MODULE,loop,[]). rpc(Pid,Request) -> Pid! {self(),Request}, receive {Pid,Respone}-> Response end.loop(X) -> receive Any-> Io:format(“Received:~p ~n”, [Any]), loop(X) end.
每当收到消息时会处理它并再次调用loop(),这一过程称为尾递归,无需消耗堆栈空间可以一直循环下去。
Erlang并发程序的错误处理建立在远程监测和处理错误的基础上,重点在补救而不是预防,几乎没有防御性代码,只有在错误后清理系统的代码,即让其他进程修复错误和任其崩溃。
程序在出错时立即崩溃的优点:
1) 不编写防御性代码,直接崩溃简单
2) 别人来修复
3) 不会使错误恶化
4) 第一时间举旗示意
5) 修复时不担心原因重在清理
6) 简化了系统架构
监视和连接类似,但监视是单向的,如果被监视的进程挂了会向监视进程发一“宕机“消息,而不是退出信号。基本错误处理函数有:
-spec spwan_link(Fun) ->Pid -spec spwan_monitor(Fun)-> {Pid,Ref} -spec process_flag(trap_exit,true) -spec link(Pid) ->true-spec unlink(Pid) -> true-spec erlang:monitor(process,Item) ->Ref -spec exit(Why) -> none()
分布式模型:分布式erlang 和基于socket的分布式模型。分布式erlang运行在可信网络,通常在同一局域网的集群上,并受防火墙保护。基于socket的分布式模型基于TCP/IP不可信网络.
分布式Erlang的主要问题在于客户端可以自行决定在服务器上分裂出多种进程,适合于你拥有全部的机器,并且想在单台机器上控制他们。lib_chan 模块让用户能够显式控制自己的机器分裂出哪些进程。
为了在互联网上执行并发程序:
1) 确保4369端口对TCP和UDP都开发,该端口保留给epmd(Erlang端口映射守护进程)
2) 选择1个或一段连续的端口给分布式erlang使用,确保这些端口开放,例如:
$erl -name …-setcookie … -kernelinet_dist_listen_min Min \
Inet_dist_listen_maxMax
Rpc提供了许多远程调用服务,global里的函数可以用来在分布式系统里注册名称以及维护一个全连接的网络。
Erlang集群就是一组带有相同cookie的互连节点。创建cookie的三种方法:
1) 在文件$HOME/.erlang.cookie存放相同的cookie
2) 在Erlang启动时,可以用 –setcookie,例如
$erl -setcookieABCDEFG2048
3) 内置函数erlang:set_cookie(node(),C)在程序中指定
Erlang通过名为端口的对象与外部程序通信,如果想端口发送一个消息,这一消息就会被发往与端口相连的外部程序,来自外部程序的消息会变成来自端口的Erlang消息。创建端口的进程成为端口的相连进程,所有发往端口的消息都必须标明相连进程的PID,所有来自外部程序的消息都会发往相连进程。
socket 编程简例
Erlang 中gen_tcp 用于编写TCP程序,gen_udp用于编写UDP程序。一个简单的TCP服务器echo示例:
Start_echo_server()-> {ok,Listen}= gen_tcp:listen(1234,[binary,{packet,4},{reuseaddr,true},{active,true}]), {ok,socket}=get_tcp:accept(Listen), gen_tcp:close(Listen), loop(Socket). loop(Socket) -> receive {tcp,Socket,Bin} -> io:format(“serverreceived binary = ~p~n”,[Bin]) Str= binary_to_term(Bin), io:format(“server (unpacked) ~p~n”,[Str]), Reply= lib_misc:string2value(Str), io:format(“serverreplying = ~p~n”,[Reply]), gen_tcp:send(Socket,term_to_binary(Reply)), loop(Socket); {tcp_closed,Socket} -> Io:format(“ServerSocket closed ~n”) end.
Tcp 的echo客户端示例:
echo_client_eval(Str) -> {Ok,Socket} = gen_tcp:connect(“localhost”,2345,[binary,{packet,4}]), ok= gen_tcp:send(Socket, term_to_binary(Str)), receive {tcp,Socket,Bin}-> Io:format(“Clientreceived binary = ~p~n”,[Bin]), Val=binary_to_term(Bin), io:format(“Clientresult = ~p~n”,[Val]), gen_tcp:close(Socket) end.
UDP server示例
udp_demo_server(Port) -> {ok,Socket}= gen_udp:open(Open,[Binary]), loop(Socket). Loop(Socket)-> receive {udp,Socket,Host,Port,Bin}-> BinReply= …, gen_udp:send(Socket,Host,Port,BinReply), loop(Socket) End.
UDP client 示例:
udp_demo_client(Request) -> {ok,Socket}= gen_udp:open(0,[Binary]), ok= gen_udp:send(Socket,”localhost”,1234,Request), Value= receive {udp,Socket,_,_,Bin}-> {ok,Bin} after2000 -> error end, gen_udp:close(Socket), Value
注意,因为UDP是不可靠的,一定要设一个超时时间,而且Reqeust最好小于500字节。
WebSocket, JS 和Erlang相结合,能够实现Web的绝大多数功能。
OTP
OTP包含了一组库和实现方式,可以构建大规模、容错和分布式的应用程序,包含了许多强大的工具,能够实现H248,SNMP等多种协议,核心概念是OTP行为,可以看作一个用回调函数作为参数的应用程序框架,类似一个J2EE容器。行为负责解决问题的非函数部分,回调函数负责解决函数部分。
通过gen_server模块可以实现事物语义和热代码交换,
1) 确定回调模块名
2) 编写接口函数
3) 在回调模块里编写6个必需的回调函数
当服务器崩溃时,需要一种机制来检测并重启它,要用到监测树,即创建一个监控器来管理服务器。监测树有两种:一对一和一对多。
$erl –boot start_sasl
会创建一个运行生产系统的环境,系统架构支持库(SASL,System Administration Support Libriaries)将负责错误记录和过载保护等工作。
使用gen_server, gen_supervisor,application等行为,可以构建可靠性为99.9999999的系统。
统一化的erlang消息:
1) 抽象了不同线路协议之间的区别
2) Erlang消息无需解析,接收进程不必先解析消息再处理,而http服务器就必须解析就收到的所有消息
3) Erlang消息可以包含任意复杂度的数据类型,而http消息必须被序列化成扁平化才能传输
4) Erlang消息可以在不同处理器之间传送
常见的第三方库有rebar(https://github.com/basho/rebar)和cowboy(https://githun.com/extend/cowboy)。 Rebar是管理erlang项目的事实标准,用户可以通过rebar创建新项目、编译项目、打包它们,以及把它们与其他项目整合在一起,同时集成了github。Cowboy是一个用erlang编写的高性能web服务器,是嵌入式web的热门实现。另外,库mochiweb2(http://github.com/mochi/mochiweb)的编码和解码方法可以实现json字符串和erlang数据类型的相互转换。
Erlang程序在多核CPU上运行
1) 使用大量进程
2) 避免副作用,例如不使用共享式ETS或DETS
3) 避免顺序瓶颈,可以选择pmap代替map
4) 小消息,大计算
5) 用mapreduce使计算并行化
mapreaduce是一个并行高阶函数,定义如下
-specmapreduce(F1,F2,Acc0,L) ->Acc F1 = fun(Pid,X) ->void F2 = fun(Key,[Value],Acc0) ->Acc L = [X] Acc = X =term()
Mapreduce 是在并行高阶函数(phofs)模块中定义的。