14  进程

14 进程

Erlang 被设计用于大规模并发。Erlang 进程是轻量级的(动态增长和缩减),具有很小的内存占用,创建和终止速度快,调度开销低。

进程通过调用 spawn() 创建

spawn(Module, Name, Args) -> pid()
  Module = Name = atom()
  Args = [Arg1,...,ArgN]
    ArgI = term()

spawn() 创建一个新进程并返回 pid。

新进程开始在 Module:Name(Arg1,...,ArgN) 中执行,其中参数是(可能为空)Args 参数列表的元素。

存在许多不同的 spawn BIFs

除了使用 pid 寻址进程之外,还有一些 BIFs 用于将进程注册到某个名称下。名称必须是一个原子,并且如果进程终止,则该名称会自动注销

BIF 描述
register(Name, Pid) 将名称 Name(一个原子)与进程 Pid 关联起来。
registered() 返回使用 register/2 注册的名称列表。
whereis(Name) 返回注册在 Name 下的 pid,如果名称未注册,则返回 undefined

表 14.1:   名称注册 BIFs

发送消息给进程时,接收进程可以使用 PID已注册名称进程别名 来识别,进程别名是 引用 类型的项。进程别名设计用于请求/回复场景。在发送回复时使用进程别名,可以让回复的接收者在操作超时或进程之间的连接丢失时阻止回复到达其消息队列。

发送消息时,可以使用 发送运算符 ! 或发送 BIFs(如 erlang:send/2)将进程别名用作接收者的标识符。只要进程别名处于活动状态,消息就会像使用创建别名进程的进程标识符一样进行传递。当别名被停用时,使用该别名发送的消息会在进入接收者的消息队列之前被丢弃。请注意,在停用时已经进入消息队列的消息 不会 被删除。

通过调用其中一个 alias/0,1 BIF,或者通过同时创建别名和监视器来创建进程别名。如果别名与监视器一起创建,则相同的引用将同时用作监视器引用和别名。通过将 {alias, _} 选项传递给 monitor/3 BIF,可以同时创建监视器和别名。 {alias, _} 选项也可以在通过 spawn_opt()spawn_request() 创建监视器时传递。

创建别名的进程可以通过调用 unalias/1 BIF 来停用进程别名。还可以根据某些事件自动停用别名。有关自动停用别名的更多信息,请参阅 alias/1 BIF 的文档和 monitor/3 BIF 的 {alias, _} 选项。

不能

  • 创建识别调用者以外的进程的别名。
  • 停用别名,除非它识别调用者。
  • 查找别名。
  • 查找由别名识别的进程。
  • 检查别名是否处于活动状态。
  • 检查引用是否是别名。

这些都是关于性能、可扩展性和分布式透明度的有意设计决策。

进程终止时,总是以 退出原因 终止。原因可以是任何项。

如果退出原因是原子 normal,则进程被称为 正常 终止。没有更多代码要执行的进程会正常终止。

发生运行时错误时,进程将以退出原因 {Reason,Stack} 终止。请参阅 退出原因

进程可以通过调用以下 BIFs 中的一个来终止自身

  • exit(Reason)
  • erlang:error(Reason)
  • erlang:error(Reason, Args)

然后进程将以原因 Reason (对于 exit/1)或 {Reason,Stack} (对于其他 BIFs)终止。

如果进程收到退出信号,并且该信号的退出原因不是 normal,则进程也会被终止,请参阅 错误处理

Erlang 进程和 Erlang 端口之间的所有通信都是通过发送和接收异步信号来完成的。最常见的信号是 Erlang 消息信号。消息信号可以使用 发送运算符 ! 发送。接收进程可以使用 receive 表达式从消息队列中获取接收到的消息。

同步通信可以分解成多个异步信号。这种同步通信的一个例子是调用 erlang:process_info/2 BIF 时,第一个参数不等于调用进程的进程标识符。调用者会发送一个异步信号请求信息,然后阻塞等待包含请求信息的回复信号。当请求信号到达目的地时,目的地进程会回复请求的信息。

进程和端口使用许多信号进行通信。以下列表包含最重要的信号。在所有请求/回复信号对的情况下,请求信号由调用特定 BIF 的进程发送,并在执行完请求的操作后发送回调用者。

在使用 发送运算符 ! 或调用 erlang:send/2,3erlang:send_nosuspend/2,3 BIFs 时发送。
在调用 link/1 BIF 时发送。
在调用 unlink/1 BIF 时发送。
在通过调用 exit/2 BIF 明确发送 exit 信号,或者 已链接的进程终止 时发送。如果信号是由于链接而发送的,则该信号会在进程释放其所有 直接可见的 Erlang 资源 后发送。
在调用 monitor/2,3 BIFs 时发送。
在调用 demonitor/1,2 BIFs 或者监视其他进程的进程终止时发送。
监控进程或端口发送,该进程或端口已终止。在进程或端口使用的所有直接可见的 Erlang 资源被释放后,会发送信号。
时间偏移更改时,由本地运行时系统上的时钟服务发送给已监控time_offset的进程。
在调用group_leader/2 BIF 时发送。
由于调用了以下 BIF 之一而发送:spawn/1,2,3,4spawn_link/1,2,3,4spawn_monitor/1,2,3,4spawn_opt/2,3,4,5spawn_request/1,2,3,4,5,或erlang:open_port/2。请求信号将发送到spawn 服务,该服务将用回复信号进行响应。
由于调用了is_process_alive/1 BIF 而发送。
由于调用了以下 BIF 之一而发送:garbage_collect/1,2erlang:check_process_code/2,3,或process_info/1,2。请注意,如果请求针对调用方本身,并且是同步请求,则不会执行任何信号传递,而是调用方在从 BIF 返回之前同步执行请求。
进程使用发送操作符 !或通过调用send() BIF 发送到本地节点上的端口。信号通过以 {Owner, {command, Data}}{Owner, {connect, Pid}}{Owner, close} 格式传递的项作为消息发送。
由于调用了以下 BIF 之一而发送:erlang:port_command/2,3erlang:port_connect/2erlang:port_close/1erlang:port_control/3erlang:port_call/3erlang:port_info/1,2。请求信号将发送到本地节点上的端口,该端口将用回复信号进行响应。
由于调用了以下 BIF 之一而发送:register/2unregister/1,或whereis/1。请求信号将发送到名称服务,该服务将用回复信号进行响应。
由于调用了以下 BIF 之一而发送:erlang:send_after/3,4erlang:start_timer/3,4,或erlang:cancel_timer/1,2。请求信号将发送到定时器服务,该服务将用回复信号进行响应。

前面提到的时钟服务、名称服务、定时器服务和 spawn 服务是运行时系统提供的服务。这些服务中的每一个都由多个独立执行的实体组成。这样的服务可以被视为一组进程,实际上也可以这样实现。由于每个服务都由多个独立执行的实体组成,因此从一个服务到一个进程发送的多个信号之间的顺序不会保留。请注意,这不会违反语言的信号排序保证

上面描述的信号的实现可能会在运行时以及由于实现更改而发生变化。您可以使用 receive 跟踪或通过检查消息队列来检测此类更改。但是,这些是运行时系统的内部实现细节,您不应依赖于这些细节。例如,上面许多回复信号都是普通的消息信号。当操作是同步时,回复信号不必是消息信号。当前实现利用了这一点,并且根据系统的状态,使用替代方法来传递回复信号。这些回复信号的实现也可能在任何时候更改为不再使用以前使用消息信号的地方。

信号是异步且自动接收的。进程不需要做任何事情来处理信号的接收,也不能做任何事情来阻止它。特别是,信号接收receive表达式的执行绑定,而可以在进程的执行流程中的任何地方发生。

当进程接收到信号时,会采取某种操作。采取的具体操作取决于信号类型、信号内容和接收进程的状态。对最常见信号采取的操作

如果消息信号是使用不再活动的进程别名发送的,则消息信号将被丢弃;否则,如果别名仍然有效或消息信号是通过其他方式发送的,则消息将添加到消息队列的末尾。当消息已添加到消息队列时,接收进程可以使用receive表达式从消息队列中获取消息。
简而言之,它可以被视为更新关于链接的进程本地信息。有关链接协议的详细说明,请参见ERTS 用户指南分布式协议章节。
将接收方设置为退出状态,丢弃信号,或将信号转换为消息并将其添加到消息队列的末尾。如果将接收方设置为退出状态,则将不再执行任何 Erlang 代码,并且进程将被调度终止。下面的接收退出信号部分详细介绍了接收到exit信号时采取的操作。
更新关于监控的进程本地信息。
如果相应的监控仍然有效,则转换为消息;否则,丢弃信号。如果信号被转换为消息,它也会被添加到消息队列的末尾。
更改进程的组领导者。
转换为消息,或根据回复以及spawn_request信号的配置方式丢弃信号。如果信号被转换为消息,它也会被添加到消息队列的末尾。有关更多信息,请参见spawn_request() BIF。
调度执行是否存活测试。如果进程处于退出状态,则是否存活测试将不会执行,直到进程使用的所有直接可见的 Erlang 资源被释放。在执行完是否存活测试后,将发送alive_reply
调度执行请求的操作。当操作执行完毕后,将发送回复信号。

请注意,当接收到信号时采取的一些操作涉及调度进一步的操作,这些操作将在这些调度操作完成后产生一个回复信号。这意味着回复信号的发送顺序可能与触发这些操作的传入信号的顺序不同。但是,这不会违反语言的信号排序保证

进程消息队列中消息的顺序反映了自将消息添加到消息队列的所有信号都将它们添加到消息队列的末尾以来与消息对应的信号接收的顺序。由于语言的信号排序保证,与来自同一发送方的信号对应的消息也按与信号发送相同的顺序排序。

如上所述,由于链接、down 信号以及由于alive_request导致的退出进程的回复信号的exit信号,直到终止进程持有的所有直接可见的 Erlang 资源被释放后才会发送。这里,直接可见的 Erlang 资源指的是语言提供的除堆数据、脏本地代码执行和终止进程的进程标识符以外的所有资源。直接可见的 Erlang 资源的示例包括注册名称ETS表。

排除的资源

进程的进程标识符只有在与该进程相关的所有内容都被释放后才能被释放以供重复使用。

当进程在 NIF 中执行脏本地代码时接收到退出信号,即使它仍在执行脏本地代码,也会被设置为退出状态。直接可见的 Erlang 资源将被释放,但运行时系统无法强制本地代码停止执行。运行时系统尝试通过以下方式来防止脏本地代码的执行影响其他进程,例如,禁用在终止进程中使用时,例如enif_send()的功能,但如果 NIF 行为不当,它仍然会影响其他进程。行为良好的脏 NIF 应该测试它正在执行的进程是否已退出,如果是,则停止执行。

在一般情况下,进程的堆在发送所有需要发送的信号之前不能被移除。堆数据占用的资源包括包含堆的内存块,但也包括从堆中引用的内容,例如堆外二进制文件,以及通过 NIF 资源对象 在堆中持有的资源。

信号发送到目标到达的时间间隔是不确定的,但为正值。如果接收方已终止,则信号不会到达,但它可以触发另一个信号。例如,发送给不存在进程的 link 信号会触发一个 exit 信号,该信号会发送回 link 信号的源头。在分布式环境中通信时,如果分布式通道断开,信号可能会丢失。

唯一提供的信号排序保证如下:如果一个实体向同一个目标实体发送多个信号,则顺序将被保留;也就是说,如果 AB 发送信号 S1,稍后又向 B 发送信号 S2,则保证 S1 不会在 S2 之后到达。请注意,S1 可能已丢失,也可能未丢失。

某些发送信号的功能在节点上本地发送时具有同步错误检查,如果接收方在发送信号时不存在,则会失败。

当一个进程通过调用 erlang:exit(self(), normal) 向自身发送一个退出信号,退出原因是 normal 时,它将被终止 exit 信号被接收到时。在所有其他情况下,当接收到一个退出原因是 normal 的退出信号时,它将被丢弃。

当一个 exit 信号被接收到,退出原因是 kill,所采取的操作取决于信号是由于链接进程终止而发送的,还是使用 exit/2 BIF 显式发送的。当使用 exit/2 BIF 发送时,信号不能被 捕获,而如果信号是由于链接而发送的,则可以被捕获。

当通过分布式通道发送信号时,即使信号应该异步发送,发送进程也可能被挂起。这是由于通道上内置的流量控制,这种流量控制或多或少一直存在。当通道的输出缓冲区大小达到 分布式缓冲区忙碌限制 时,在缓冲区大小缩小到限制以下之前,在通道上发送的进程将被挂起。

根据缓冲区满的原因,挂起的进程恢复之前所需的时间可能 差异很大。例如,这可能导致对 erpc:call() 的调用的超时时间显著延迟。

由于此功能已经存在很长时间,因此无法删除它,但可以在每个进程级别上使用 process_flag(async_dist, Bool) 来启用 完全异步的分布式信号传递,这可以用于解决由于阻塞信号传递而导致的问题。但是,请注意,您需要确保使用 完全异步的分布式信号传递 发送的数据的流量控制已实现,或者此类数据的数量始终是有限的;否则,您可能会遇到内存使用量过大的情况。

可以通过调用 erlang:system_info(dist_buf_busy_limit) 来检查 分布式缓冲区忙碌限制 的大小。

上述不规则性无法修复,因为它们已经成为 Erlang 的一部分太久了,修复它们会导致很多现有代码崩溃。

两个进程可以彼此 链接。同样,在同一个节点上的进程和端口也可以彼此链接。如果其中一个进程调用 link/1 BIF,并将另一个进程的进程标识符作为参数,则可以在两个进程之间创建链接。还可以使用以下 spawn BIF 之一创建链接:spawn_link()spawn_opt()spawn_request()。在这种情况下,spawn 操作和 link 操作将原子地执行。

如果链接的参与者之一终止,它将 发送一个退出信号 给另一个参与者。退出信号将包含已终止参与者的 退出原因

可以通过调用 unlink/1 BIF 来移除链接。

链接是双向的,两个进程之间只能有一个链接。对 link() 的重复调用没有效果。参与的任何一个进程都可以创建或移除链接。

链接用于监控其他进程的行为,请参见 错误处理

Erlang 具有一个用于进程之间错误处理的内置功能。终止进程会向所有链接的进程发出退出信号,这些进程也可以终止或以某种方式处理退出。此功能可用于构建分层程序结构,其中一些进程监督其他进程,例如,在进程异常终止时重启它们。

请参见 OTP 设计原则,了解有关使用此功能的 OTP 监督树的更多信息。

当一个进程或端口 终止 时,它将向所有与之 链接 的进程和端口发送退出信号。退出信号将包含以下信息

已终止进程或端口的进程或端口标识符。

发送退出信号的进程或端口的进程或端口标识符。

此标志将被设置,表示退出信号是由于链接而发送的。

已终止进程或端口的退出原因,或者原子

  • noproc,如果在先前对 link(PidOrPort) BIF 的调用中设置链接时,未找到进程或端口。被标识为退出信号发送方的进程或端口将等于传递给 link/1PidOrPort 参数。

  • noconnection,如果链接的进程驻留在不同的节点上,并且节点之间的连接丢失或无法建立。在这种情况下,被标识为退出信号发送方的进程或端口可能仍然存活。

也可以通过调用 exit(PidOrPort, Reason) BIF 显式发送退出信号。退出信号将发送给由 PidOrPort 参数标识的进程或端口。发送的退出信号将包含以下信息

调用 exit/2 的进程的进程标识符。

发送退出信号的进程或端口的进程或端口标识符。

此标志不会被设置,表示此退出信号不是由于链接而发送的。

在调用 exit/2 时作为 Reason 传递的项。如果 Reason 是原子 kill,则接收方无法 捕获退出 信号,并且将在接收到信号时无条件终止。

进程接收到退出信号时会发生什么取决于

  • 接收方在接收到退出信号时 捕获退出 的状态。

  • 退出信号的退出原因。

  • 退出信号的发送方。

  • 退出信号的 link 标志的状态。如果 link 标志被设置,则退出信号是由于链接而发送的;否则,退出信号是通过调用 exit/2 BIF 发送的。

  • 如果 link 标志被设置,则当接收到退出信号时,还取决于 链接是否仍然处于活动状态

根据上述状态,进程接收到退出信号时将发生以下情况

  • 如果退出信号被静默丢弃,则

    • 退出信号的 link 标志被设置,并且相应的链接已被停用。

    • 退出信号的退出原因是原子 normal,接收方未捕获退出,并且接收方和发送方不是同一个进程。

  • 如果接收进程被终止,则

    • 退出信号的 link 标志未被设置,并且退出信号的退出原因是原子 kill。接收进程将以原子 killed 作为退出原因终止。

    • 接收方未捕获退出,并且退出原因不是原子 normal。此外,如果退出信号的 link 标志被设置,则链接也需要处于活动状态,否则退出信号将被丢弃。接收进程的退出原因将等于退出信号的退出原因。请注意,如果 link 标志被设置,则退出原因 kill 不会被转换为 killed

    • 退出信号的退出原因是原子 normal,并且退出信号的发送方与接收方是同一个进程。在这种情况下,link 标志不能被设置。接收进程的退出原因将是原子 normal

  • 如果接收方正在捕获退出,则退出信号将被转换为消息信号并添加到接收方的消息队列末尾,退出信号的 link 标志是

    • 未被设置,并且信号的退出原因不是原子 kill

    • 设置,对应的链接处于活动状态。请注意,在这种情况下,退出原因kill**不会**终止进程,也不会转换为killed

    转换后的消息将采用{'EXIT', SenderID, Reason}的形式,其中Reason等于退出信号的退出原因,SenderID是发送退出信号的进程或端口的标识符。

**监控器**是链接的另一种选择。进程Pid1可以通过调用BIF erlang:monitor(process, Pid2)Pid2创建监控器。该函数返回一个引用Ref

如果Pid2以退出原因Reason终止,则会向Pid1发送一个'DOWN'消息。

{'DOWN', Ref, process, Pid2, Reason}

如果Pid2不存在,则会立即发送'DOWN'消息,Reason设置为noproc

监控器是单向的。对erlang:monitor(process, Pid)的重复调用会创建多个独立的监控器,并且当Pid终止时,每个监控器都会发送一个'DOWN'消息。

可以通过调用erlang:demonitor(Ref)删除监控器。

可以为具有注册名称的进程创建监控器,也可以在其他节点上创建监控器。

每个进程都有自己的进程字典,可以通过调用以下BIF访问:

put(Key, Value)
get(Key)
get()
get_keys(Value)
erase(Key)
erase()