一、概述
与大多数的进程相反,Erlang中的并发很廉价,派生出一个进程就跟面向对象的语言中分配一个对象的开销差不多。 在启动一个复杂的运算时,启动运算、派生进程以及返回结果后,所有进程神奇的烟消云散,它们的内存、邮箱、所持有的数据库句柄、它们打开的套接字,以及一些不乐意手工清理的东西,都一并消失。
Erlang进程不是操作系统进程,它们由erlang运行时系统实现,比线程要轻量的多,单个erlang系统可以轻易地派生出成百上千个进程。运行时系统中所有的进程都是隔离的,单个进程的内存不与其他进程共享,也不会被濒死或跑疯的进程破坏。
在操作系统中,典型的线程会在地址空间中为自己预留数兆的栈空间(也就是说32位的机器上并发线程最多也就几千个),栈空间溢出便会导致崩溃。erlang进程在启动时栈空间只有几百个字节,并会按需伸缩。
二、示例
1 14> Pid = spawn(fun() -> timer:sleep(60000), primes:primelist(100000) end). %% 派生一个进程,等待1分钟后做素数运算。 素数功能代码已提前编写好。 2 <0.55.0> 3 15> erlang:process_info(Pid). 4 [{current_function,{timer,sleep,1}}, 5 {initial_call,{erlang,apply,2}}, 6 {status,waiting}, 7 {message_queue_len,0}, 8 {messages,[]}, 9 {links,[]}, 10 {dictionary,[]}, 11 {trap_exit,false}, 12 {error_handler,error_handler}, 13 {priority,normal}, 14 {group_leader,<0.26.0>}, 15 {total_heap_size,233}, 16 {heap_size,233}, %%刚启动的erlang进程所占的堆的大小仅为233字节,栈的大小10个字节。 17 {stack_size,10}, 18 {reductions,43}, %%创建erlang进程仅消耗了43个reductions, 可见erlang的轻量。 19 {garbage_collection,[{min_bin_vheap_size,46422}, 20 {min_heap_size,233}, 21 {fullsweep_after,65535}, 22 {minor_gcs,0}]}, 23 {suspending,[]}] 24 17> erlang:statistics(run_queue). %% 进程处于挂起状态,堆、栈、时间片消耗都不会变 25 0 26 22> erlang:statistics(run_queue). %% 进程进入运行队列,准备被调度。 27 1 28 23> erlang:process_info(Pid). 29 [{current_function,{primes,'-primelist/3-lc$^0/1-0-',2}}, 30 {initial_call,{erlang,apply,2}}, 31 {status,runnable}, 32 {message_queue_len,0}, 33 {messages,[]}, 34 {links,[]}, 35 {dictionary,[]}, 36 {trap_exit,false}, 37 {error_handler,error_handler}, 38 {priority,normal}, 39 {group_leader,<0.26.0>}, 40 {total_heap_size,393300}, 41 {heap_size,75113}, %%进程在做素数计算, 堆栈大小随着需要开始增加。 消耗的时间片也会随着计算量增加而增加。 42 {stack_size,13773}, 43 {reductions,89874499}, 44 {garbage_collection,[{min_bin_vheap_size,46422}, 45 {min_heap_size,233}, 46 {fullsweep_after,65535}, 47 {minor_gcs,3085}]}, 48 {suspending,[]}] 49 27> erlang:statistics(run_queue). 50 0 51 29> erlang:process_info(Pid). 52 [{current_function,{primes,'-primelist/3-lc$^0/1-0-',2}}, 53 {initial_call,{erlang,apply,2}}, 54 {status,runnable}, 55 {message_queue_len,0}, 56 {messages,[]}, 57 {links,[]}, 58 {dictionary,[]}, 59 {trap_exit,false}, 60 {error_handler,error_handler}, 61 {priority,normal}, 62 {group_leader,<0.26.0>}, 63 {total_heap_size,393300}, 64 {heap_size,75113}, 65 {stack_size,87}, %% 随着计算的结束, 栈空间开始收缩, 这里可以看到堆空间没有变,堆的分配是由erlang GC来控制的, 进程结束时由GC来回收。 66 {reductions,958602600}, 67 {garbage_collection,[{min_bin_vheap_size,46422}, 68 {min_heap_size,233}, 69 {fullsweep_after,65535}, 70 {minor_gcs,33117}]}, 71 {suspending,[]}]
三、进程的调度
由于Erlang虚拟机对SMP的支持,每个操作系统线程都可以运行在一个调度器上,每个调度器拥有一自己的运行队列,这样避免了多个调度器同时调度在运行队列中的任务产生的冲突,但是如何保证调度队列任务分配的公平性,Erlang引入了一个高效和公平的概念,迁移逻辑。迁移逻辑利用在系统中收集的统计数据,控制和平衡了队列。
Erlang启动模拟器的时候可以加上+S去指定最大调度器数和可用调度器数。可用调度器数可以在模拟器运行时更改。
-> erl +S 16:8 Erlang R16B03-1 (erts-5.10.4) [source] [64-bit] [smp:16:8] [async-threads:10] [hipe] [kernel-poll:false] Eshell V5.10.4 (abort with ^G) 1> erlang:system_info(schedulers_online). 8 2> erlang:system_info(schedulers). 16
下面我们尝试起多个erlang进程去做素数运算,然后看看调度队列情况。
-> erl +S 8:8 %%启动8个调度器同时可用。 Erlang R16B03-1 (erts-5.10.4) [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] Eshell V5.10.4 (abort with ^G) 1> erlang:statistics(run_queue). %%之前没有任务进入调度队列 0 2> [spawn(fun() -> timer:sleep(5000), primes:primelist(10000) end) || _ <- lists:seq(1, 10)]. %% 同时启动10个进程做素数运算 [<0.36.0>,<0.37.0>,<0.38.0>,<0.39.0>,<0.40.0>,<0.41.0>, <0.42.0>,<0.43.0>,<0.44.0>,<0.45.0>] 5> erlang:statistics(run_queue). %%同时有7个进程被调度运行,3个进程出去准备状态 3 7> erlang:statistics(run_queue). %%两个任务被换入。 1 8> erlang:statistics(run_queue). %%任务执行完毕,没有任务处于等待状态。 0
那么问题来了,在同样的硬件环境下,是不是调度器越多,进程处理的速度越快呢?列出测试结果:
-> erl +S 4:4 %%%启动4个可用调度器 Erlang R16B03-1 (erts-5.10.4) [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] 2> [spawn(fun() -> {P1, _P2} = timer:tc(primes, primelist, [100000]), io:format("timer:~p~n", [P1]) end) || _ <- lists:seq(1, 10)]. timer:185078651 timer:190735178 timer:192956743 timer:193186850 timer:220074562 timer:222929652 timer:234756209 timer:235304593 timer:235474721 timer:236500425 -> erl +S 8:8 %%%启动8个可用调度器 Erlang R16B03-1 (erts-5.10.4) [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] Eshell V5.10.4 (abort with ^G) 1> 1> 1> 1> [spawn(fun() -> {P1, _P2} = timer:tc(primes, primelist, [100000]), io:format("timer:~p~n", [P1]) end) || _ <- lists:seq(1, 10)]. [<0.35.0>,<0.36.0>,<0.37.0>,<0.38.0>,<0.39.0>,<0.40.0>, <0.41.0>,<0.42.0>,<0.43.0>,<0.44.0>] timer:187405676 timer:187568120 timer:188255698 timer:188577806 timer:190819642 timer:191208176 timer:235470698 timer:236842370 timer:237630863 timer:238206383 -> erl +S 16:11 Erlang R16B03-1 (erts-5.10.4) [source] [64-bit] [smp:16:11] [async-threads:10] [hipe] [kernel-poll:false] Eshell V5.10.4 (abort with ^G) 1> [spawn(fun() -> {P1, _P2} = timer:tc(primes, primelist, [100000]), io:format("timer:~p~n", [P1]) end) || _ <- lists:seq(1, 10)]. [<0.35.0>,<0.36.0>,<0.37.0>,<0.38.0>,<0.39.0>,<0.40.0>, <0.41.0>,<0.42.0>,<0.43.0>,<0.44.0>] timer:243000833 timer:243636514 timer:244753411 timer:245005027 timer:245296405 timer:245356679 timer:245659526 timer:245662159 timer:245731926 timer:245779971
从测试结果看,并不是调度器越多越好,也不是越少越好,合适实际应用场景才是最好的。