在了解Erlang的并发机制之前,我们先来看一下Erlang与Java的并发性能对比,一个是并发单元的创建时间,一个是并发单元之间的消息通讯时间(纵坐标代表时间,横坐标代表并发数量):
(测试程序及说明见这里,原测试时间比较早了,于是在自己的虚拟机上重新跑了下(CenterOS 6,3G内存);JVM生成线程数量控制,可见这里,)
从上面的测试结果来看,Erlang的并发效率要比Java好很多,不在一个数量级上,Erlang是Java的1000倍左右(这不能说明在所有场景下Erlang的并发性能都优于Java,比如消息传递方面,消息的大小可能对这个结果也有影响)。那么Erlang的并发机制相比Java到底好在哪里,本文及后续几篇文章的目的就是搞清楚这个问题。
在《Erlang 编程指南》一书中,对Erlang的并发单元,进程的解释如下:
“Erlang进程是轻量级进程,它的生成、上下文切换和消息传递是由虚拟机管理的。操作系统线程的Erlang进程之间没有任何联系,这使并发有关的操作不仅独立于底层的操作系统,而且也是非常高效和具有很强可扩展性。”
由此可见,Erlang里的进程跟操作系统里常提到的进程,线程完全没有关系,只是Erlang并发机制里基本并发单元的一个代称。下面详细的说明Erlang进程的创建过程。
从Erlang虚拟机的角度来看,Erlang进程就是一个process结构,定义在$OTP_SRC/erts/emulator/beam/erl_process.h中(struct process),该结构中包含进程所使用的堆栈、GC、调度、消息队列等信息。Erlang程序通过erlang:spawn或者相关BIF调用(spawn_opt,spawn_link等)可以生成一个新的进程。
在erts中,spawn调用会最终调用到spawn_3:
[$OTP_SRC/erts/emulator/beam/bif.c --> spawn_3]
pid = erl_create_process(BIF_P, BIF_ARG_1, BIF_ARG_2, BIF_ARG_3, &so);
首先就是调用erl_create_process来生成一个新的进程,会返回创建进程的pid。我们来看看erl_create_process具体都做了哪些事情。
[$OTP_SRC/erts/emulator/beam/erl_process.c --> erl_create_process]
// 参数说明 Eterm erl_create_process(Process* parent, /* Parent of process (default group leader). */ Eterm mod, /* Tagged atom for module. */ Eterm func, /* Tagged atom for function. */ Eterm args, /* Arguments for function (must be well-formed list). */ ErlSpawnOpts* so) /* Options for spawn. */ // 首先调用alloc_process分配一个process结构需要的内存空间 p = alloc_process(); /* All proc locks are locked by this thread on success */ if (!p) { // 如果p==null,则说明系统中进程数量已达到上限 so->error_code = SYSTEM_LIMIT; goto error; } /* Scheduler queue mutex should be locked when changeing * prio. In this case we don't have to lock it, since * noone except us has access to the process. */ // 设置process的最小堆大小 if (so->flags & SPO_USE_ARGS) { //以参数值设置堆属性,一般通过erlang:spawn_opt调用传入参数 p->min_heap_size = so->min_heap_size; // 最小堆内存 p->min_vheap_size = so->min_vheap_size; // 最小虚拟堆内存 //(用于存放二进制数据) p->prio = so->priority; // 进程优先级(max, high, normal, low) p->max_gen_gcs = so->max_gen_gcs; // full gc之前可进行的minor gc的最大次数 } else { // 按默认值设置:H_MIN_SIZE=233 (fib(11),erlang里的堆内存按照fib系列增长, // 具体可参见[$OTP_SRC/erts/emulator/beam/erl_c.c --> erts_init_gc]里的说明) p->min_heap_size = H_MIN_SIZE; p->min_vheap_size = BIN_VH_MIN_SIZE; // 默认值32768(216) p->prio = PRIORITY_NORMAL; p->max_gen_gcs = (Uint16) erts_smp_atomic32_read_nob(&erts_max_gen_gcs); } // 创建进程时传入的module,function,和参数的数量 p->initial[INITIAL_MOD] = mod; p->initial[INITIAL_FUN] = func; p->initial[INITIAL_ARI] = (Uint) arity; /* * Must initialize binary lists here before copying binaries to process. */ p->off_heap.first = NULL; p->off_heap.overhead = 0; // 计算初始需要的heap大小 heap_need += IS_CONST(parent->group_leader) ? 0 : NC_HEAP_SIZE(parent->group_leader); if (heap_need < p->min_heap_size) { sz = heap_need = p->min_heap_size; } else { /* * Find the next heap size equal to or greater than the given size (if offset == 0). * * If offset is 1, the next higher heap size is returned (always greater than size). */ sz = erts_next_heap_size(heap_need, 0); } // 分配进程堆内存 p->heap = (Eterm *) ERTS_HEAP_ALLOC(ERTS_ALC_T_HEAP, sizeof(Eterm)*sz); p->old_hend = p->old_htop = p->old_heap = NULL; // 进程堆内存里年轻代的标志位:地址小于此标志位的,是较老的年轻代(一般情况 // 下,这些对象至少经过了一次minor gc或者major gc);大于这个地址的是较年轻的 // 年轻代。 p->high_water = p->heap; // minor gc的次数 p->gen_gcs = 0; // 栈顶,紧邻堆 p->stop = p->hend = p->heap + sz; p->htop = p->heap; p->heap_sz = sz; /* No need to initialize p->fcalls. */ // 当前模块及函数信息 p->current = p->initial+INITIAL_MOD; // 第一条指令设置为i_apply p->i = (BeamInstr *) beam_apply; // cp保存进入一个函数调用时,当前函数的下一条指令 p->cp = (BeamInstr *) beam_apply+1; // 消息队列 p->msg.first = NULL; p->msg.last = &p->msg.first; p->msg.save = &p->msg.first; p->msg.len = 0; #ifdef ERTS_SMP // 消息进入队列 p->msg_inq.first = NULL; p->msg_inq.last = &p->msg_inq.first; p->msg_inq.len = 0; p->bound_runq = NULL; if (so->flags & SPO_LINK) { // 进程链接,由spawn_link指定 if (IS_TRACED_FL(parent, F_TRACE_PROCS)) { trace_proc(parent, parent, am_link, p->id); } // 父进程及当前进程互相连接 erts_add_link(&(parent->nlinks), LINK_PID, p->id); erts_add_link(&(p->nlinks), LINK_PID, parent->id); } /* * Test whether this process should be initially monitored by its parent. */ if (so->flags & SPO_MONITOR) { Eterm mref; //进程监控:单向,由spawn_monitor指定 mref = erts_make_ref(parent); erts_add_monitor(&(parent->monitors), MON_ORIGIN, mref, p->id, NIL); erts_add_monitor(&(p->monitors), MON_TARGET, mref, parent->id, NIL); so->mref = mref; } /* * Schedule process for execution. */ if (!((so->flags & SPO_USE_ARGS) && so->scheduler)) // 如果参数中未指定scheduler,则使用父进程的任务队列 rq = erts_get_runq_proc(parent); else { // 根据绑定的scheduler,获取任务队列(spawn_opt中可以绑定scheduler,文档 // 中无说明,具体可见:[$OTP_SRC/erts/emulator/beam/bif.c --> spawn_opt_1]) int ix = so->scheduler-1; ASSERT(0 <= ix && ix < erts_no_run_queues); rq = ERTS_RUNQ_IX(ix); p->bound_runq = rq; } #ifdef ERTS_SMP // 设置当前进程的任务队列 p->run_queue = rq; #endif // 设置进程的状态为waiting p->status = P_WAITING; // 将当前进程添加到任务队列,并将进程的状态设置为runnable notify_runq = internal_add_to_runq(rq, p); // 唤醒调度器 smp_notify_inc_runq(notify_runq); // 返回进程PID res = p->id; // 创建成功 VERBOSE(DEBUG_PROCESSES, ("Created a new process: %T\n",p->id));
进程创建成功后,会将pid返回到spawn_3调用。
[$OTP_SRC/erts/emulator/beam/bif.c --> spawn_3]
if (ERTS_USE_MODIFIED_TIMING()) { BIF_TRAP2(erts_delay_trap, BIF_P, pid, ERTS_MODIFIED_TIMING_DELAY); } BIF_RET(pid);
BIF_TRAP2是Erlang里的Trap机制,关于Trap机制的详细说明见这里。这里的调用属于第三类,主动放弃CPU。erts_delay_trap最终会以以pid和ERTS_MODIFIED_TIMING_DELAY()为参数调用erlang:delay_trap。ERTS_USE_MODIFIED_TIMING()这个宏成立的条件是modified timing开关打开,具体参数erl的+T参数,默认未打开。更详细的说明见这里。
[$OTP_SRC/ erts/preloaded/src /erlang.erl --> erlang:delay_trap]
%% %% Trap function used when modified timing has been enabled. %% delay_trap(Result, 0) -> erlang:yield(), Result; delay_trap(Result, Timeout) -> receive after Timeout -> Result end.erlang:yield 等同于 receive after 1 -> Result end , delay_trap 的作用就是让当前进程放弃 CPU ,使其它的进程有机会运行,在 spawn 调用的场景下,也就是会使新创建的进程有机会被调度到。