8.处理 | 8. Processes
8 过程
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/1
BIF,可以看出深层列表只需要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/1
并erts_debug:flat_size/1
返回相同的值。共享已经失去。
在未来的Erlang/OTP版本中,它可能被实现为(可选)保存共享的方式。
8.3 SMP仿真器
SMP仿真器(在R11B中引入)通过运行几个Erlang调度程序线程(通常与核心数相同)利用多核或多CPU计算机。每个调度程序线程都以与非SMP模拟器中的Erlang调度程序相同的方式调度Erlang进程。
为了通过使用SMP模拟器获得性能,您的应用程序
大部分时间必须具有多个可运行的Erlang进程
。否则,Erlang仿真器当时仍然只能运行一个Erlang进程,但您仍然必须支付锁定开销。尽管Erlang/OTP试图尽可能减少锁定开销,但它永远不会完全为零。
似乎并行的基准通常是连续的。例如,基准测试是完全顺序的。所以是“戒指基准”的最常见的实现;通常一个过程是活跃的,而其他过程是在一个receive
声明中等待。