[Erlang0009][OTP] 高效指南 进程

原文链接:http://www.erlang.org/doc/efficiency_guide/processes.html

错误之处欢迎指正。

8 进程

 

8.1 创建Erlang进程

 

相比操作系统的线程和进程来说,Erlang的进程更为轻量。

 

一个新创建的进程在non-SMP、不支持HiPE的虚拟机上占用内存309words。(支持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

这里包括233words的堆空间(也包括栈)。垃圾回收器会按需增加堆的大小。


进程的主循环必须是尾递归。否则,在进程退出之前,栈空间会不同的增长。

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的过程中,它的地址每次都会推到栈里去。正确的尾递归版本的函数应该是这样的:

DO

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

初始的堆大小

默认的233words的初始堆空间在一个有成百上千乃至上万的Erlang系统中是非常保守的。垃圾回收器会按需调整堆空间的大小。

 

在一个较少进程组成的系统中,用erl的+h选项或者在每个进程启动的时候使用spawn_opt/4的min_heap_size选项都可以增加最小堆的大小以提高性能。

 

这里也存在两面性的问题:首先,虽然垃圾回收器可以增大堆空间,但这个过程是一步一步增加的,这比在进程被创建时直接创建一个较大的堆空间要代价更高。但是,如果进程的堆空间比其存储的数据多出很多,垃圾回收器也会回收空间;设置最小堆空间来避免这个问题。

 

注意:虚拟机可能会用更大的内存。而且因为垃圾回收器不经常光顾,大的二进制数据可能会存留更长的时间。

 

在一个很多进程组成的系统里,时间很短的计算工作可以指派给一个新的进程,分配的最少堆空间可以多一些。当进工作完成时,把计算结果发送给另一个进程然后退出。如果最小堆空间能够被准确计算,那么进程就不比做任何垃圾回收操作。但在没有经过适当评测时这类优化不应被使用。

 

8.2 进程消息

 

进程之间的所有消息数据的传递都是拷贝,除了同一个节点的refc binary(见《二进制的构造和匹配(1)》)。

 

当消息发送到另一个节点的进程时,会先编码成Erlang External Format,然后通过TCP/IP套接字来发送。接受的节点会解码消息,并发送到相应的进程。

 

常量池

 

Erlang terms常量现在被保存在常量池里;每一个被加载的模块都有自己的池。下面的函数:

DO (in R12B and later)

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

不会在每次调用的时候都创建元组,(只有在下次垃圾回收之后才会重新创建),元组会一直保存在模块的常量池里。

 

但是如果常量被发送给另一个进程(或者储存在ETS表中),它就会被拷贝。原因是,运行时系统必须能够持续追踪所有常量的引用,以便能够正确卸载包含常量的代码。(当代码被卸载时,常量会被拷贝到引用它们的进程的堆上。)常量的拷贝或许会在以后的发布版本中被淘汰。

 

被抛弃的共享

 

共享的子项将不再被保存,当一个项被发送给另一个进程,或在调用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/0创建了一个深列表。如果我们调用list_to_binary/1,我们可以将这个列表转换成1024字节的二进制:

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

用erts_debug:size/1 BIF,我们可以看到这个列表占有22words的堆空间:

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, []).
17
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发布版本中,我们可能会实现保存共享部分的方法。我们不打算把保存共享作为默认的功能,因为那将对绝大多数的Erlang应用不利。

 

8.3 SMP虚拟机

 

SMP虚拟机会给运行了多个Erlang调度线程(与核的个数相同)的多核或多CPU计算机带来好处。每个调度线程都会像未开SMP的虚拟机调度线程一样来调度进程。

 

想要通过SMP虚拟机在性能上获益,你的应用必须在绝大多数情况下跑在多个进程上。否则Erlang虚拟机还是只能在同一时间运行一个Erlang进程。但同时你必须承担锁的额外代价。尽管我们极力减少锁的代价,但它不可能降为零。

 

那些看起来是并发的基准经常都是顺序的。例如estone基准,它是完全顺序的。就是最普通的环基准的实现;通常当一个进程被激活时,其他进程在receive状态中等待。

 

percept应用可被用来测试你的应用,看是否有并发的潜力

原文地址:https://www.cnblogs.com/liangjingyang/p/2708546.html