非常教程

Erlang 20参考手册

指南:效率指南 | Guide: Efficiency guide

8.处理 | 8. Processes

8.1创建一个Erlang进程

与操作系统中的线程和进程相比,Erlang进程是轻量级的。

新产生的Erlang进程在非HiMP支持的非SMP模拟器中使用309个字的内存。(SMP支持和HiPE支持都添加到此大小。)大小可以找到如下:

Erlang (BEAM) emulator version 5.6 [async-threads:0] [kernel-poll:false]

Eshell V5.6  (abort with ^G)
1> Fun = fun() -> receive after infinity -> ok end end.
#Fun<...>
2> {_,Bytes} = process_info(spawn(Fun), memory).
{memory,1232}
3> Bytes div erlang:system_info(wordsize).
309

大小包括233个字的堆区(包括堆栈)。垃圾收集器根据需要增加堆。

进程的主(外)循环必须是尾递归的。否则,堆栈会一直增长,直到进程终止。

不要

loop() -> 
  receive
     {sys, Msg} ->
         handle_sys_msg(Msg),
         loop();
     {From, Msg} ->
          Reply = handle_msg(Msg),
          From ! Reply,
          loop()
  end,
  io:format("Message is processed~n", []).

调用io:format/2永远不会被执行,但是每次loop/0递归地调用返回地址仍然会被推送到堆栈。该函数的正确尾递归版本如下所示:

loop() -> 
   receive
      {sys, Msg} ->
         handle_sys_msg(Msg),
         loop();
      {From, Msg} ->
         Reply = handle_msg(Msg),
         From ! Reply,
         loop()
 end.

初始堆大小

默认的初始堆大小为233字对于支持包含数十万甚至数百万个进程的Erlang系统是相当保守的。垃圾收集器根据需要增长和缩小堆。

在使用相对较少的处理的系统中,性能可能通过使用的增加最小堆大小来改善+h选项erl或有关使用的处理的每次过程基础min_heap_size为选项spawn_opt/4

收益是双重的:

  • 虽然垃圾收集器增长了堆,但它逐步增长,这比直接建立一个更大的堆时产生的成本更高。
  • 如果垃圾收集器比存储在其上的数据量大得多,则垃圾收集器也可以收缩堆; 设置最小堆大小可以防止这种情况。

警告

模拟器可能会使用更多的内存,并且因为垃圾收集发生得不那么频繁,所以巨大的二进制文件可以保存更长时间。

在具有许多进程的系统中,短时间运行的计算任务可以衍生为具有较高最小堆大小的新进程。当进程完成时,它将计算结果发送到另一个进程并终止。如果最小堆大小被正确计算,那么进程可能根本不需要做任何垃圾回收。如果没有适当的测量,这种优化不会被尝试。

8.2进程消息

Erlang进程之间的消息中的所有数据都被复制,除了refc binaries在同一个Erlang节点上。

当一个消息被发送到另一个Erlang节点上的进程时,它首先被编码为Erlang外部格式,然后通过TCP / IP套接字发送。接收Erlang节点解码消息并将其分发到正确的进程。

常量池

常量Erlang术语(也称为文字)保存在常量池中; 每个加载的模块都有自己的池。下面的函数在每次调用时都不会构建元组(只在下次运行垃圾收集器时才放弃),但元组位于模块的常量池中:

days_in_month(M) ->
    element(M, {31,28,31,30,31,30,31,31,30,31,30,31}).

但是,如果一个常量被发送到另一个进程(或存储在Ets表中),它将被复制。原因在于运行时系统必须能够跟踪所有对常量的引用,以正确地卸载包含常量的代码。(当代码被卸载时,常量被复制到引用它们的进程的堆中。)在将来的Erlang / OTP版本中,常量的复制可能会被消除。

失去分享

在下列情况下不会保留共享子项:

  • 当一个术语被发送到另一个进程时
  • 当一个术语作为spawn调用中的初始过程参数传递时
  • 当一个术语存储在Ets表中时

这是一个优化。大多数应用程序不会使用共享子项发送消息。

下面的示例演示如何创建共享子项:

kilo_byte() ->
    kilo_byte(10, [42]).

kilo_byte(0, Acc) ->
    Acc;
kilo_byte(N, Acc) ->
    kilo_byte(N-1, [Acc|Acc]).

kilo_byte/1创建一个深度列表。如果list_to_binary/1被调用,深度列表可以转换为1024字节的二进制:

1> byte_size(list_to_binary(efficiency_guide:kilo_byte())).
1024

使用erts_debug:size/1BIF,可以看出深层列表只需要22个字的堆空间:

2> erts_debug:size(efficiency_guide:kilo_byte()).
22

erts_debug:flat_size/1如果共享被忽略,使用BIF可以计算深度列表的大小。当它被发送到另一个进程或存储在Ets表中时,它就成为列表的大小:

3> erts_debug:flat_size(efficiency_guide:kilo_byte()).
4094

可以验证,如果将数据插入到Ets表中,共享将会丢失:

4> T = ets:new(tab, []).
#Ref<0.1662103692.2407923716.214181>
5> ets:insert(T, {key,efficiency_guide:kilo_byte()}).
true
6> erts_debug:size(element(2, hd(ets:lookup(T, key)))).
4094
7> erts_debug:flat_size(element(2, hd(ets:lookup(T, key)))).
4094

当数据通过Ets表时,erts_debug:size/1erts_debug:flat_size/1返回相同的值。共享已经失去。

在未来的Erlang/OTP版本中,它可能被实现为(可选)保存共享的方式。

8.3 SMP仿真器

SMP仿真器(在R11B中引入)通过运行几个Erlang调度程序线程(通常与核心数相同)利用多核或多CPU计算机。每个调度程序线程都以与非SMP模拟器中的Erlang调度程序相同的方式调度Erlang进程。

为了通过使用SMP模拟器获得性能,您的应用程序大部分时间必须具有多个可运行的Erlang进程。否则,Erlang仿真器当时仍然只能运行一个Erlang进程,但您仍然必须支付锁定开销。尽管Erlang/OTP试图尽可能减少锁定开销,但它永远不会完全为零。

似乎并行的基准通常是连续的。例如,基准测试是完全顺序的。所以是“戒指基准”的最常见的实现;通常一个过程是活跃的,而其他过程是在一个receive声明中等待。

Erlang 20

Erlang 是一种通用的面向并发的编程语言,可应付大规模开发活动的程序设计语言和运行环境。

主页 https://www.erlang.org/
源码 https://github.com/erlang/otp
版本 20
发布版本 20.1