在计算机科学中,它是一个并行计算的数学模型,最初为由大量独立的微处理器组成的高并行计算机所开发,Actor模型的理念非常简单: 天下万物皆为Actor。Actor之间通过发送消息来通信,消息的传送是异步的,通过一个邮件队列(mail queue)来处理消息。每个Actor 是完全独立的,可以同时执行它们的操作。每一个Actor是一个计算实体,映射接收到的消息到以下动作: 1. 发送有限个消息给其它Actor 2. 创建有限个新的Actor 3. 为下一个接收的消息指定行为
以上三种动作并没有固定的顺序,可以并发地执行。Actor会根据接收到的消息进行不同的处理。
简而言之: 一个Actor指的是一个最基本的计算单元,它能接收一个消息并且基于其执行计算。
综上,我们知道可以把系统中的所有事物都抽象成一个Actor,那么在一个系统中,可以将一个大规模的任务分解为一些小任务,这些小任务
可以由多个Actor并发处理,从而减少任务的完成时间和任务复杂度。
为什么会在讲Erlang进程的时候讲Actor模型的概念,就是因为对于Erlang的并发编程模型正是基于Actor模型,Erlang的代码运行在
进程中,而进程就是Erlang称呼Actor的方式,Eralng也是最著名的使用Actor规则的编程的语言。
在Erlang的进程不是我们传统上的进程,Erlang进程是轻量级进程,它的生成、上下文切换和消息传递是由虚拟机管理的,操作系统 线程进程和Erlang进程之间没有任何联系,这使并发有关的操作不仅独立于底层的操作系统,而且也是非常高效和具有很强可扩展性。 它运行在 Erlang 虚拟机上,非常小,非常轻,可以瞬间创建上万,甚至几十万个,进程间完全是独立的内存空间执行,不共享内存, 这些独立的内存空间可以独立的进行垃圾回收,基于独立运行,在发生错误的时候也是隔离的,其他不相关的进程可以继续运行。 在进程运行时若出现错误,由于进程的轻量级,Erlang 采取的措施是“任其崩溃”和“让其他进程修复”。 在Erlang上查看默认限制数量是26万多,可以进行修改。每个进程创建后都会有一个独一无二的 Pid,这些进程之间通过 Pid 来互相发 送消息,进程的唯一交互方式也是消息传递,消息也许能被对方收到,也许不能,收到后可以处理该消息。消息发送是异步的如果想知道某 个消息是否被进程收到,必须向该进程发送一个消息并等待回复。
Erlang 中的并发编程只需要如下几个简单的函数。
Pid = spawn(Mod,Func, Args)
创建一个新的并发进程来执行Mod模块中的 Fun(),Args 是参数。 跟上面提供的spawn/3功能相同的函数还有:
spawn(Fun) -> pid()
spawn(Node, Fun) -> pid()
spawn(Module, Function, Args) -> pid()
spawn(Node, Module, Function, Args) -> pid()
spawn_link(Fun) -> pid()
spawn_link(Node, Fun) -> pid()
spawn_link(Module, Function, Args) -> pid()
spawn_link(Node, Module, Function, Args) -> pid()
spawn_monitor(Fun) -> {pid(), reference()}
spawn_monitor(Module, Function, Args) -> {pid(), reference()}
spawn_opt(Fun, Options) -> pid() | {pid(), reference()}
spawn_opt(Node, Fun, Options) -> pid() | {pid(), reference()}
spawn_opt(Module, Function, Args, Options) ->pid() | {pid(), reference()}
spawn_opt(Node, Module, Function, Args, Options) ->pid() | {pid(), reference()}
创建好进程,返回对应的Pid之后向就可以向进程进程发送消息,erlang用 “!”来发送消息,格式如下。notice:消息发送是异步的, 发送方不等待而是继续之前的工作。
Pid !Message,
Pid1 ! Pid2 ! Pid3 ! Pid..n ! Message.
erlang用 receve ... end 来接受发送给某个进程的消息,匹配后处理,格式如下。
receive
Pattern1 [when Guard1] ->
Expression1;
Pattern2 [when Guard2] ->
Expression2;
...
after T ->
ExpressionTimeout
end
某个消息到达后,会先与 Pattern 进行匹配,匹配相同后执行,若未匹配成功消息则会保存起来待以后处理,进程会开始下一轮操作, 若等待超时T,则会执行表达式 ExpressionTimeout。
有些时候使用通过进程Pid来标识进程需要维护进程Pid,出于某些原因维护进程Pid,不方便灵活,比如你给某个服务器进程请求数据, 你还得考虑怎么得到服务器进程的Pid,有些时候进程由于某种异常重启后Pid会发生变化,如果没有及时同步机制,会导致功能异常, 于是乎Erlang提供了一套进程注册管理的机制----注册进程Erlang中管理注册进程的有4个内置函数,register、unregister、 whereis、registered,它们的用法如下: 1)register(Atom, Pid):将一个进程Pid注册一个名为AnAtom的原子,如果原子AnAtom已经被另一个注册进程所使用, 那么注册就会失败。 2)unregister(Atom):移除与AnAtom相对应进程的所有注册信息。如果一个注册死亡,那么它也会被自动取消注册。 3)whereis(Atom) -> Pid | undefined:判断AnAtom是否已经被其他进程注册。如果成功,则返回进程标识符Pid。 如果AnAtom没有与之相对应的进程,那么就返回原子undefined。 4)registered() -> [AnAtom ::atom()]:返回一个系统中所有已经注册的名称列表。
Erlang 对于进程处理理念之一是“任其崩溃”和“让其他进程修复”,常规Erlang系统中有很多进程同时运行,进程之间可能相互依赖, 这么复杂的情况之下怎么实现该理念呢?Erlang除了提供exception,try catch等语法,还支持Link和Monitor两种监控进程的机制, 使得所有进程可以连接起来,组成一个整体。当某个进程出错退出时,其他进程都会收到该进程退出的消息通知。有了这些特点,使用erlang 建立一个简单,并且健壮的系统就不是什么难事。
相关API link(Pid), A进程调用了link(Pid) 则A进程与Pid之间就建立起了双向连接,如果两个进程相连接,如果其中一个终止时, 讲发送exit信号给另一方,使其终止,同时终止进程会依次发送exit信号给所有与其连接的进程,这使得exit信号在系统内层层蔓延。 该函数连接不存在的进程时会导致发起连接的进程终止 spawn_link()系列函数 它与link(Pid)的差别就是 原子性与非原子性 unlink(Pid) 移除调用进程与Pid的连接 通过调用process_flag(trap_exit, true)可以设置捕捉exit信号, 假如有A,B两个进程且彼此link 总结... 1.当A的结束原因是normal时(进程正常执行完就是normal),B是不会退出的,此时link机制不发生作用 2.若A的结束原因是killed,例如调用exit(PidA,kill) ,则无论B是否有设置trap_exit,B都会terminate,此时退出信号捕捉机制是无效的 3.若A的结束原因不是normal也不是killed(例如exit(PidA,Reason)),那么B在设置了trap_exit时,会捕捉到退出信号, 取而代之的是收取到一条消息{‘EXIT’,Pid,Reason},这时B不会结束,用户可以根据收到的消息对A进程的结束进行处理;若B没有设置trap_exit,B就会terminate
捕获状态 | 退出信号(原因) | 动作 |
---|---|---|
false | normal | 不做任何事 |
false | kill | 消亡,向链接的进程广播退出信号(killed) |
false | X | 消亡,向链接的进程广播退出信号X |
true | normal | 接收到{'EXIT', Pid, nomal} |
true | kill | 消亡,向链接的进程广播退出信号(killed) |
true | X | 将{'EXIT', Pid, X} 加入到邮箱 |
相关API monitor(process, monitor_process_identifier()) %monitor_process_identifier() 为Pid或者已注册的进程名称 demonitor(MonitorRef) demonitor(MonitorRef, OptionList) 监视器与link不同的是它是单向式观察一些进程终止,各个监视器通过Erlang的引用相互区分,是调用monitor返回的,具有唯一性, 而且A进程可以设置多个对B进程的监视器,每一个通过不同的引用区分。 当被监视的进程终止时,一条格式{'Down',Reference, process, Pid, Reason}的消息会被发给监视此进程的进程 调用erlang:demonitor(Reference)可以移除监视器, 调用erlang:demonitor(Reference,[flush])可以让该监视进程邮箱中所有与Reference对应的{'DOWN', Reference,process,Pid,Reason} 的消息被冲刷掉。 如果尝试监视一个不存在的进程会导致收到一条{'DOWN', process, Pid,Reason}的消息,其中Reason为noproc,这和link()不一样
就目前计算机体系结构而言,任何进程或线程要执行就需要得到CPU资源,对于erlang的进程同样如此。erlang虚拟机同时存在成千上万的进程, 但是cpu核心数又是有限的,所有erlang并发特性就需要一个合适的调度规则来安排各个进程的运行, 简单而言,erlang虚拟机调度程序保留两个队列,准备好运行的就绪队列以及等待接收消息的进程的等待队列。当等待队列中的进程收到消息或获 得超时时,它将被移动到就绪队列。调度程序从就绪队列中选择第一个进程并将其交给BEAM执行一个时间片。当时间片用完时,BEAM会抢占正在 运行的进程,并将进程添加到就绪队列的末尾。如果在时间片用完之前在接收中阻止了进程,则会将其添加到等待队列中。
Erlang调度器主要有以下特点:
Reduction 受操作系统中基于时间片调度算法的影响,一开始知道有reduction这个概念时,一直想搞清楚这个reduction到底对应多长的绝对时间,不过, 从Erlang本身对reduction的使用来看,完全没有必要纠结这个问题。《Erlang编程指南》一书中对reduction的说明如下: 程序中的每一个命令,无论它是一个函数调用,还是一个算术操作,或者内置函数,都会分配一定数量的reduction。虚拟机使用这个值来衡量一个 进程的活动水平。
进程优先级 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系统中,进程之间的通信是通过消息传递来完成的。消息使用Pid ! Message的形式发送,通过receive语句获取。每个Erlang进程 都有用来存储传入消息的信箱。当一个消息发送的时候,它会从发送进程中拷贝到接收进程的信箱,并以它们到达的时间次序存储。消息的传递是 异步的,一个发送进程不会在发送消息后被暂停。
上面提到发送消息时,会在两个进程之间存在消息复制,为什么需要复制呢?这就跟进程的堆内存有关。虽然在Erlang的文档(heap_type)中 说明堆内存有三种类型:private,shared,hybrid,但是在实际的代码中,只有两种private和hybrid (参见[$R15B_OTP_SRC/erts/emulator/beam/erl_bif_info.c --> system_info_1]), (参见[$R15B_OTP_SRC/erts/Makefile.in:# Until hybrid is nofrag, don't build it.), 也就是说Erlang目前的堆内存只有一种:private。 private类型的堆内存是跟shared类型相对的:shared是指所有线程共享同一块内存(比如Java),多个线程对同一块内存的访问需要锁保护; 而private类型的堆内存是指每个进程独享一块内存,对于内存的访问不需要锁保护。 在Erlang的private堆内存架构下,发送消息需要做三件事件:
在shared堆内存架构下,发送消息只需要O(1)(只传递消息地址),那为什么Erlang要默认选择private类型的堆内存呢? 其实这跟后面要讲到的Erlang的GC相关:private的优势就是GC的延迟很低,可以很快的完成(因为只保存一个进程的数据, GC扫描时的数据量很小)。在SMP环境下,实际上每个进程有两个消息队列。进程发送消息时,实际上消息是添加到目标进程的公有队列 (通过锁来保证互斥访问);而目标进程在消费消息时,实际上是在自己的私有消息队列上处理的,从而减小锁带来的访问开销。但是, 如果目标进程在自己的私有消息队列上无法匹配到消息,那么公有队列中的消息将被添加到私有队列。
receive
Pattern1 [when Guard1] ->
Expression1;
Pattern2 [when Guard2] ->
Expression2;
...
after T ->
ExpressionTimeout
end
整个过程如下
erlang 进程GC Memory Layout 内存分布
在我们深入垃圾回收机制之前,我们先来看看Erlang进程的内存布局. 一个Erlang进程的内存布局通常分为是三个部分(有人认为是四个部分, 把mailbox作为单独的一个部分), 进程控制块, 堆和栈,和普通的Linux进程的内存布局非常类似.
Shared Heap Erlang Process Memory Layout
+----------------------------------+ +----------------------------------+
| | | |
| | | PID / Status / Registered Name | Process
| | | | Control
| | | Initial Call / Current Call +----> Block
| | | | (PCB)
| | | Mailbox Pointers |
| | | |
| | +----------------------------------+
| | | |
| | | Function Parameters |
| | | | Process
| | | Return Addresses +----> Stack
| | | |
| +--------------+ | | Local Variables |
| | | | | |
| | +------------+--+ | +-------------------------------+--+
| | | | | | | |
| | | +-------------+--+ | | ^ v +----> Free
| | | | | | | | | Space
| | | | +--------------+-+ | +--+-------------------------------+
| +-+ | | | | | |
| +-+ | Refc Binary | | | Mailbox Messages (Linked List) |
| +-+ | | | |
| +------^---------+ | | Compound Terms (List, Tuples) | Process
| | | | +----> Private
| | | | Terms Larger than a word | Heap
| | | | |
| +--+ ProcBin +-------------+ Pointers to Large Binaries |
| | | |
+----------------------------------+ +----------------------------------+
进程控制块: 进程控制块持有关于进程的一些信息, 比如PID, 进程状态(running, waitting), 进程注册名, 初始和当前调用, 指向进程mailbox的指针
栈: 栈是向下增长的, 栈持有函数调用参数,函数返回地址,本地变量以及一些临时空间用来计算表达式.
堆: 堆是向上增长的, 堆持有进程的mailbox, 复合terms(Lists, Tuples, Binaries),以及大于一个机器字的对象(比如浮点数对象). 大于64个字节的二进制terms,被称为Reference Counted Binary, 他们不是存在进程私有堆里面,他们是存在一个大的共享堆里,所有进程 都可以通过指向RefC Binary的指针来访问该共享堆,RefC Binary指针本身是存在进程私有堆里面的.
GC Details 为了更准确的解释默认的Erlang垃圾回收机制, 实际上运行在每个独立Erlang进程内部的是分代拷贝垃圾回收机制, 还有一个引用计数的 垃圾回收运行在共享堆上.
Private Heap GC 私有堆垃圾回收 私有堆的垃圾回收是分代的. 分代机制把进程的堆内存分为两个部分,年轻代和年老代. 区分是基于这样一个考虑, 如果一个对象在运行 一次垃圾回收之后没有被回收,那么这个对象短期内被回收的可能性就很低. 所以, 年轻代就用来存储新分配的数据,年老代就用来存放运行 一定次数的垃圾回收之后依然幸存的数据. 这样的区分可以帮助GC减少对那些很可能还不是垃圾的数据不必要的扫描. 对应于此, Erlang的 GC扫描有两个策略, Generational(Minor) 和 Fullsweep(Major). Generational GC只回收年轻代的区域, 而Fullsweep则同时回收年轻代和 年老代.
下面我们一起来review一下一个新创建的Erlang进程触发GC的步骤, 假设以下不同的场景:
场景 1:
Spawn > No GC > Terminate 假设一个生存期较短的进程, 在存活期间使用的堆内存也没有超过 min_heap_size,那么在进程结束是全部内存即被回收.
场景 2:
Spawn > Fullsweep > Generational > Terminate 假设一个新创建的进程,当进程的数据增长超过了min_heap_size时, fullsweep GC即被触发, 因为在此之前还没有任何GC被触发,所以堆区 还没有被分成年轻代和年老代. 在第一次fullsweep GC结束以后, 堆区就会被分为年轻代和年老代了, 从这个时候起, GC的策略就被切换为 generational GC了, 直到进程结束.
场景 3:
Spawn > Fullsweep > Generational > Fullsweep > Generational > ... > Terminate 在某些情景下, GC策略会从generation再切换回fullsweep. 一种情景是, 在运行了一定次数(fullsweep_after)的genereration GC之后, 系统会再次切换回fullsweep. 这个参数fullsweep_after可以是全局的也可以是单进程的. 全局的值可以通过函数erlang:system_info(fullsweep_after)获取, 进程的可以通过函数erlang:process_info(self(),garbage_collection)来获取. 另外一种情景是, 当generation GC(minor GC)不能够收集到足够的内存空间时. 最后一种情况是, 当手动调用函数garbage_collector(PID)时. 在运行fullsweep之后, GC策略再次切换回generation GC直到以上的任意一个情景再次出现.
场景 4:
Spawn > Fullsweep > Generational > Fullsweep > Increase Heap > Fullsweep > ... > Terminate 假设在场景3里面,第二个fullsweep GC依然没有回收到足够的内存, 那么系统就会为进程增加堆内存, 然后该进程就回到第一个场景,像刚创建的进程一样首先 开始一个fullsweep,然后循环往复.
那么对Erlang来说, 既然这些垃圾回收机制都是自动完成的, 为什么我们需要花时间去了解学习呢? 首先, 通过调整GC的策略可以使你的系统运行的更快. 其次, 了解GC可以帮助我们从GC的角度来理解为什么Erlang是一个软实时的系统平台. 因为每个进程有自己的私有内存空间和私有GC,所以每次GC发生的时候只在进程 内部进行,只stop本进程, 不会stop其他进程,这正是一个软实时系统所需要的.
Shared Heap GC 共享堆垃圾回收 共享堆的GC是通过引用计数来实现的. 共享堆里面的每个对象都有一个引用计数,这个计数就是表示该对象被多少个Erlang进程持有(对象的指针存在进程的私有堆里). 如果一个对象的引入计数变成0的时候就表示该对象不可访问可以被回收了.
进程调度
就目前计算机体系结构而言,任何进程或线程要执行就需要得到CPU资源,对于erlang的进程同样如此。erlang虚拟机同时存在成千上万的进程, 但是cpu核心数又是有限的,所有erlang并发特性就需要一个合适的调度规则来安排各个进程的运行, 简单而言,erlang虚拟机调度程序保留两个队列,准备好运行的就绪队列以及等待接收消息的进程的等待队列。当等待队列中的进程收到消息或获 得超时时,它将被移动到就绪队列。调度程序从就绪队列中选择第一个进程并将其交给BEAM执行一个时间片。当时间片用完时,BEAM会抢占正在 运行的进程,并将进程添加到就绪队列的末尾。如果在时间片用完之前在接收中阻止了进程,则会将其添加到等待队列中。
Erlang调度器主要有以下特点:
进程调度运行在用户空间 :Erlang进程不同于操作系统进程,Erlang的进程调度也跟操作系统完全没有关系,是由Erlang虚拟机来完成的;
调度是抢占式的:每一个进程在创建时,都会分配一个固定数目的reduction(这个数量默认值是2000),每一次操作(函数调用), reduction就会减少,当这个数量减少到0时或者进程没有匹配的消息时,抢占就会发生(无视优先级);
每个进程公平的使用CPU:每个进程分配相同数量的reduction,可以保证进程可以公平的(不是相等的)使用CPU资源
调度器保证软实时性:Erlang中的进程有优先级,调度器可以保证在下一次调度发生时,高优先级的进程可以优先得到执行。
What operators does Erlang have?
Arithmetic operators: + - * / div rem
Comparison operators: =:= == =/= /= > >= < =<
Logical operators: and andalso or orelse
Bitwise operators: bsl bsr Bitwise logical operators: band Bor bxor bnot