[Erlang-0009][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应用可被用来测试你的应用,看是否有并发的潜力。