简要对比
Erlang和Go虽然在实现及功能上差异较大,但是都支持高并发的轻量级用户任务(Erlang的轻量进程,Go的Goroutine), 并且都采用了消息传递的方式作为任务间交互的方式。
在Erlang中,采用了一种比较纯粹的消息传递机制,进程间几乎没有任何形式的数据共享,只能通过彼此间发送消息进行通信; 而Go虽然是基于共享内存的,但是也必须通过消息传递来进行共享数据的同步。 可以说消息传递机制是两种语言任务间交互的首要方式。
但是在具体实现中,鉴于两种语言的差异,也表现为不同的形式:
- 在Erlang中,进程之间以彼此的Pid作为标识进行消息的发送,一切数据都仅可以消息的形式在进程间复制
- 在Go中,不同的Goroutine间通过共享的channel进行通信,由于Go本质上是建立在共享存储模型上的,因此全局变量、参数甚至是一部分栈变量都是可以共享的,通信一般控制在较小的规模,仅用来保证共享的同步语义
下面将分别就Erlang和Go各自实现分别进行分析介绍。
Erlang中的消息传递机制
语法
消息传递是Erlang语言并发机制的基础。在Erlang中,主要包含以下并发原语:
- 创建一个新的并发轻量进程,用于执行函数Fun,并返回其进程标识符Pid:
Pid = spawn(Fun) |
- 向标识符为Pid的进程发送消息(注: 由于
Pid ! M
的返回值是消息M本身,因此可以用类似Pid1 ! Pid2 ! Pid3 ! … M
的语法来向多个进程发送同一个消息):
Pid ! Message |
- 用
receive ... end
来接收一个发送给当前进程的消息,语法如下(注: 当一个进程接收到一个消息时,依次尝试与Pattern1
(及Guard1
),Pattern2
(及Guard2
),… 进行模式匹配,若成功,则对相应的Expressions
求值,否则继续后续匹配):
receive Pattern1 [when Guard1] -> Expressions1; Pattern2 pGuard2Expressions2; ... ... end |
- 对于接收操作,我们还可以为其设置一个超时控制,一旦超过某个预设的时长仍没有消息到达,则执行相应的超时操作,语法如下:
sleepT) -> receive T -> true end |
以上就是Erlang中基本的消息传递的接口。
内部实现
相对于Go而言,Erlang的发展历史更加悠久,因此代码复杂程度要大大高于Go这门新型语言。 因此在分析过程中,主要还是以介绍实现机制为主,具体的数据结构及源代码实现就不作过分细致的剖析了。
Erlang的BEAM解释器的代码位于otp/erts/emulator/beam/
路径下, 其中与Send相关的代码位于otp/erts/emulator/beam/erl_message.c
中, 而与Receive相关的代码则位于otp/erts/emulator/beam/beam_emu.c
中。
之所以不在一处实现,是因为Erlang把接收操作作为一种BEAM基本指令来实现,而发送操作则以内部函数的方式实现。
在具体实现中,Erlang采用了消息Copy的方法实现消息传递——消息从发送进程的堆拷贝到接收进程的堆:
- 在发送时,如果接收进程正在另一个调度器上执行,或者有其他并发的进程也在向其发送消息时,本次发送就存在竞争风险,不能直接完成
- 发送者会在堆上为接收进程分配一个临时区域并将消息拷贝到该区(该内存区将在后续进行垃圾收集时合并到接收进程的堆空间)
- 拷贝完成后,会将指向这块新区域的指针链入接收进程的消息队列
- 如果接收进程正处于阻塞态,则将其唤醒并添加到就绪队列中
在SMP版Erlang虚拟机中,进程的消息队列由两部分组成—— 一个公共队列和一个私有队列。 公共队列用于接收其他进程发送的消息,通过互斥锁加以保护;私有队列用来减少对锁的争用: 接收进程首先在接收队列中查找符合匹配的消息,如果没有,再从公共队列中复制消息到私有队列中。
(在非SMP的Erlang虚拟机中,进程只有一个消息队列。)
接收进程发现当前任务队列上没有匹配的消息后,会跳转执行wait_timeout: 设置一个定时器并阻塞在该定时器上—— 当该定时器触发或者有新消息到来时,都会唤醒接收进程。
由于进程的消息缓冲是以队列形式维护的,因此从发送进程角度来看,可以认为消息缓冲的大小是无限的, 因此Send操作一般不会阻塞,这点注意与后面要将的Go消息传递机制相区别。 这也就是为什么Erlang中仅对Receive操作提供超时响应机制的原因了!
高级特性
与Go主要面向SMP服务器应用不同,Erlang是一种面向分布式集群环境的编程语言, 因此消息传递除了支持本地进程间的通信外,还支持分布式环境的进程间通信。
基本流程是:
- 首先通过Pid(或在分布式环境上的等价概念)查询“名字服务”
- 找到该远程进程所在的远程主机地址信息,进而通过套接字进行后续发送操作
- 远程Erlang虚拟机进程接收到消息,再进一步分析,将其派发到指定的进程消息队列中
进一步的实现细节会涉及Erlang分布式处理的机制,这里就不展开分析了,待后续单开主题讨论。
Go中channel机制介绍
在Go中,channel结构是Goroutine间消息传递的基础,属于基本类型,在runtime库中以C语言实现。 Go中针对channel的操作主要包括以下几种:
- 创建:
ch = make(chan int,N)
- 发送:
ch <- l
- 接收:
l <- ch
- 关闭:
close(ch)
ch = make(chan int,N)
ch <- l
l <- ch
close(ch)
另外,还可以通过select语句同时在多个channel上进行收发操作,语法如下:
select { case ch01 <- x: ... ... /* do something ... */ y ch02default/* no event ... */ } |
此外,基于select的操作tai还支持超时控制,具体的语法示例如下: