教程集 www.jiaochengji.com
教程集 >  Golang编程  >  golang教程  >  正文 Golang和Erlang消息传递机制对比

Golang和Erlang消息传递机制对比

发布时间:2021-05-06   编辑:jiaochengji.com
教程集为您提供Golang和Erlang消息传递机制对比等资源,欢迎您收藏本站,我们将为您提供最新的Golang和Erlang消息传递机制对比资源

上一篇文章介绍了 Go 和 Erlang 在调度器的实现,本文将简要介绍这两种并发语言的消息传递机制

简要对比

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 p[when Guard2] ->
      Expressions2;
  ... ...
end
  • 对于接收操作,我们还可以为其设置一个超时控制,一旦超过某个预设的时长仍没有消息到达,则执行相应的超时操作,语法如下:
receive
  Pattern1 [when Guard1] ->
      Expressions1;
  Pattern2 p[when Guard2] ->
      Expressions2;
        ... ...
after Time ->
  ExpressionTimeout
end
  • 可以建立一个只包含超时的接收操作,事实上就相当于一个延时操作:
sleep(T) ->
  receive
      after 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)

另外,还可以通过select语句同时在多个channel上进行收发操作,语法如下:

select {
  case ch01 <- x:
      ... ... /* do something ... */
  case y <- ch02:
      ... ... /* do something ... */
  default:
      ... ... /* no event ... */
}

此外,基于select的操作tai还支持超时控制,具体的语法示例如下:

select {
  case v := <- ch:
      ... ...
  case <- time.After(5 * time.Second):
      ... ...
}

尽管Go以消息传递作为Goroutine间交互的主要方式,但是基于channel的通信又必须依赖channel引用的共享才能得以实现, 因此Go语言绝不是一种纯粹的消息传递语言。 一般而言,channel的引用可以通过以下几种方式在不同Goroutine间共享:

  • “父Goroutine” 的栈变量,通过用Go语句创建Goroutine时的参数进行传递
ch = make (chan int)
go fn (ch)
l <- ch
  • “父Goroutine” 的栈变量,Go创建的Goroutine以闭包作为执行函数,栈变量自动共享
ch = make (chan int)
go func () { i = 1; chan <- i; } ()
x <- chan
  • 由于Go的垃圾收集器认为channel的引用只能在栈上,因此一般不用全局的引用进行共享

另外,在创建channel时,还可以指定是否采用buffer及buffer的大小,默认buffer为0 。 当buffer大于0时,可以进行异步的消息传递:接收方只有在当前buffer为空时才阻塞,而发送方则只有在buffer满时才阻塞。 详细情形将在后面介绍。

内部实现

核心数据结构

Go中channel的实现代码在src/pkg/runtime/chan.c中,其核心数据结构如下 (对于gccgo,其实现代码在libgo/runtime/chan.c中,由于使用不同的C编译器及语法,因而数据结构实现略有不同):

struct Hchan
{
  uintgo   count;        // total data in q
  uintgo   dataqsize;    // size of the circular q
  uint16   elemsize;
  uint16   pad;      // ensures proper alignment of the buffer that follows Hchan in memory
  bool    closed;
  Alg*  elemalg;  // interface for element type
  uintgo   sends;        // send index
  uintgo   recvx;        // receive index
  WaitQ    recvq;        // list of recv waiters
  WaitQ    sendq;        // list of send waiters
  Lock;
}

我们可以按照表示属性还是表示状态将Hchan的内部成员进行分类并逐一分析。

表示channel属性的成员如下,这些成员在用make进行初始化后确定,并且在后续操作中不会变化:

Golang和Erlang消息传递机制对比
什么是RabbitMQ?RabbitMQ的简单介绍
Golang错误和异常处理的正确姿势
java中RabbitMQ集群使用方法简单介绍
Runtime 中的消息机制
使用Go语言一段时间的感受
Java、PHP、Python、Erlang、Golang 千万级内存数据插入、查询性能对比
Go 开发关键技术指南 | 为什么你要选择 Go?(内含超全知识大图)
golang知识点
深入理解Golang之Context(可用于实现超时机制)

[关闭]
~ ~