erlang各种有用的函数包括一些有用nif封装,还有一些性能测试case。
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.
 
 
 
 
 
 

27 KiB

Erlang 进程相关学习

目录

  1. actor模型和进程特性
  2. 进程创建
  3. 进程监控与注册
  4. 进程调度
  5. 进程发送消息
  6. 进程接收消息
  7. 进程GC
  8. 更多

actor模型和进程特性

actor模型

在计算机科学中,它是一个并行计算的数学模型,最初为由大量独立的微处理器组成的高并行计算机所开发,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规则的编程的语言。

Eralng进程特性

在Erlang的进程不是我们传统上的进程,Erlang进程是轻量级进程,它的生成、上下文切换和消息传递是由虚拟机管理的,操作系统 线程进程和Erlang进程之间没有任何联系,这使并发有关的操作不仅独立于底层的操作系统,而且也是非常高效和具有很强可扩展性。 它运行在 Erlang 虚拟机上,非常小,非常轻,可以瞬间创建上万,甚至几十万个,进程间完全是独立的内存空间执行,不共享内存, 这些独立的内存空间可以独立的进行垃圾回收,基于独立运行,在发生错误的时候也是隔离的,其他不相关的进程可以继续运行。 在进程运行时若出现错误,由于进程的轻量级,Erlang 采取的措施是“任其崩溃”和“让其他进程修复”。 在Erlang上查看默认限制数量是26万多,可以进行修改。每个进程创建后都会有一个独一无二的 Pid,这些进程之间通过 Pid 来互相发 送消息,进程的唯一交互方式也是消息传递,消息也许能被对方收到,也许不能,收到后可以处理该消息。消息发送是异步的如果想知道某 个消息是否被进程收到,必须向该进程发送一个消息并等待回复。

进程创建

Erlang 中的并发编程只需要如下几个简单的函数。

Pid = spawn(ModFunc 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} 加入到邮箱

监视器(monitor)

相关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调度器主要有以下特点:

  1. 进程调度运行在用户空间 :Erlang进程不同于操作系统进程,Erlang的进程调度也跟操作系统完全没有关系,是由Erlang虚拟机来完成的;
  2. 调度是抢占式的:每一个进程在创建时,都会分配一个固定数目的reduction(这个数量默认值是2000),每一次操作(函数调用), reduction就会减少,当这个数量减少到0时或者进程没有匹配的消息时,抢占就会发生(无视优先级);
  3. 每个进程公平的使用CPU:每个进程分配相同数量的reduction,可以保证进程可以公平的(不是相等的)使用CPU资源
  4. 调度器保证软实时性: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堆内存架构下,发送消息需要做三件事件:

  1. 计算消息的大小,并在接收进程的内存空间中给消息分配内存;
  2. 将消息的内容拷贝到接收进程的堆内存中;
  3. 最后将消息的地址添加到接收进程的消息队列。 从上面的步骤可以看出,拷贝消息的代码是O(n),n是消息的长度,也就是说消息越长,花费越大。所以在使用Erlang时,要避免大数据量的大消息传递。

在shared堆内存架构下,发送消息只需要O(1)(只传递消息地址),那为什么Erlang要默认选择private类型的堆内存呢? 其实这跟后面要讲到的Erlang的GC相关:private的优势就是GC的延迟很低,可以很快的完成(因为只保存一个进程的数据, GC扫描时的数据量很小)。在SMP环境下,实际上每个进程有两个消息队列。进程发送消息时,实际上消息是添加到目标进程的公有队列 (通过锁来保证互斥访问);而目标进程在消费消息时,实际上是在自己的私有消息队列上处理的,从而减小锁带来的访问开销。但是, 如果目标进程在自己的私有消息队列上无法匹配到消息,那么公有队列中的消息将被添加到私有队列。

进程接收消息

receive
	Pattern1 [when Guard1] ->
		Expression1;
	Pattern2 [when Guard2] ->
		Expression2;
	...
	after T ->
		ExpressionTimeout
end

整个过程如下

  1. 当我们输入receive语句时,我们启动一个计时器(如果有after T)。
  2. 获取邮箱中的第一个消息,并尝试将其与Pattern1、Pattern2等进行匹配。 如果匹配成功,则从邮箱中删除消息,并计算模式后面的表达式。
  3. 如果receive语句中的任何模式都不匹配邮箱中的第一个消息,那么第一个消息将从邮箱中删除并放入“save队列”中。 然后尝试邮箱中的第二条消息。重复此过程,直到找到匹配的消息或检查邮箱中的所有消息为止。
  4. 如果邮箱中的所有消息都不匹配,则进程将被挂起,并在下次将新消息放入邮箱时重新安排执行时间。注意,当新消息到达时, 保存队列中的消息不会重新匹配;只匹配新消息( Erlang的实现是非常“聪明”的,并且能够最小化每个消息被接收方的receive测试的次数)
  5. 一旦匹配了消息,那么所有放入save队列的消息都将按照到达进程的顺序重新进入邮箱。如果设置了计时器, 则清除计时器。
  6. 如果计时器在等待消息时超时,则计算表达式ExpressionsTimeout,并按到达进程的顺序将任何保存的消息放回邮箱。

进程GC

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调度器主要有以下特点:

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

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

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

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

  5. 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