图解Go的channel底层原理

废话不多说,直奔主题。

channel的整体结构图

640?wx_fmt=png

简单说明:

  • buf是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表

  • sendx和 recvx用于记录 buf这个循环链表中的~发送或者接收的~index

  • lock是个互斥锁。

  • recvq和 sendq分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表

源码位于 /runtime/chan.go中(目前版本:1.11)。结构体为 hchan

type hchan struct {	
    qcount   uint           // total data in the queue	
    dataqsiz uint           // size of the circular queue	
    buf      unsafe.Pointer // points to an array of dataqsiz elements	
    elemsize uint16	
    closed   uint32	
    elemtype *_type // element type	
    sendx    uint   // send index	
    recvx    uint   // receive index	
    recvq    waitq  // list of recv waiters	
    sendq    waitq  // list of send waiters	
    // lock protects all fields in hchan, as well as several	
    // fields in sudogs blocked on this channel.	
    //	
    // Do not change another G's status while holding this lock	
    // (in particular, do not ready a G), as this can deadlock	
    // with stack shrinking.	
    lock mutex	
}

下面我们来详细介绍 hchan中各部分是如何使用的。

先从创建开始

我们首先创建一个channel。

ch := make(chan int, 3)

640?wx_fmt=png

创建channel实际上就是在内存中实例化了一个 hchan的结构体,并返回一个ch指针,我们使用过程中channel在函数之间的传递都是用的这个指针,这就是为什么函数传递中无需使用channel的指针,而直接用channel就行了,因为channel本身就是一个指针。

channel中发送send(ch <- xxx)和recv(<- ch)接收

先考虑一个问题,如果你想让goroutine以先进先出(FIFO)的方式进入一个结构体中,你会怎么操作?加锁!对的!channel就是用了一个锁。hchan本身包含一个互斥锁 mutex

channel中队列是如何实现的

channel中有个缓存buf,是用来缓存数据的(假如实例化了带缓存的channel的话)队列。我们先来看看是如何实现“队列”的。还是刚才创建的那个channel

ch := make(chan int, 3)

640?wx_fmt=png

当使用 send(ch<-xx)或者 recv(<-ch)的时候,首先要锁住 hchan这个结构体。

640?wx_fmt=png

然后开始 send(ch<-xx)数据。一

ch <- 1

ch <- 1

ch <- 1

这时候满了,队列塞不进去了 动态图表示为:640?wx_fmt=gif

然后是取 recv(<-ch)的过程,是个逆向的操作,也是需要加锁。

640?wx_fmt=png

然后开始 recv(<-ch)数据。一

<-ch

<-ch

<-ch

图为:640?wx_fmt=gif

注意以上两幅图中 bufrecvx以及 sendx的变化, recvxsendx是根据循环链表 buf的变动而改变的。至于为什么channel会使用循环链表作为缓存结构,我个人认为是在缓存列表在动态的 sendrecv过程中,定位当前 send或者 recvx的位置、选择 send的和 recvx的位置比较方便吧,只要顺着链表顺序一直旋转操作就好。

缓存中按链表顺序存放,取数据的时候按链表顺序读取,符合FIFO的原则。

send/recv的细化操作

注意:缓存链表中以上每一步的操作,都是需要加锁操作的!

每一步的操作的细节可以细化为:

  • 第一,加锁

  • 第二,把数据从goroutine中copy到“队列”中(或者从队列中copy到goroutine中)。

  • 第三,释放锁

每一步的操作总结为动态图为:(发送过程)640?wx_fmt=gif

或者为:(接收过程)640?wx_fmt=gif

所以不难看出,Go中那句经典的话: Donotcommunicatebysharing memory;instead,share memorybycommunicating.的具体实现就是利用channel把数据从一端copy到了另一端!还真是符合 channel的英文含义:

640?wx_fmt=gif

当channel缓存满了之后会发生什么?这其中的原理是怎样的?

使用的时候,我们都知道,当channel缓存满了,或者没有缓存的时候,我们继续send(ch <- xxx)或者recv(<- ch)会阻塞当前goroutine,但是,是如何实现的呢?

我们知道,Go的goroutine是用户态的线程( user-space threads),用户态的线程是需要自己去调度的,Go有运行时的scheduler去帮我们完成调度这件事情。关于Go的调度模型GMP模型我在此不做赘述,如果不了解,可以看我另一篇文章(Go调度原理)

goroutine的阻塞操作,实际上是调用 send(ch<-xx)或者 recv(<-ch)的时候主动触发的,具体请看以下内容:

//goroutine1 中,记做G1	
ch := make(chan int, 3)	
ch <- 1	
ch <- 1	
ch <- 1

640?wx_fmt=png

640?wx_fmt=png

这个时候G1正在正常运行,当再次进行send操作(ch<-1)的时候,会主动调用Go的调度器,让G1等待,并从让出M,让其他G去使用

640?wx_fmt=png

同时G1也会被抽象成含有G1指针和send元素的 sudog结构体保存到hchan的 sendq中等待被唤醒。

640?wx_fmt=gif

那么,G1什么时候被唤醒呢?这个时候G2隆重登场。

640?wx_fmt=png

G2执行了recv操作 p:=<-ch,于是会发生以下的操作:

640?wx_fmt=gif

G2从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中。

640?wx_fmt=gif

假如是先进行执行recv操作的G2会怎么样?

你可能会顺着以上的思路反推。首先:

640?wx_fmt=png

这个时候G2会主动调用Go的调度器,让G2等待,并从让出M,让其他G去使用。G2还会被抽象成含有G2指针和recv空元素的 sudog结构体保存到hchan的 recvq中等待被唤醒

640?wx_fmt=gif

此时恰好有个goroutine G1开始向channel中推送数据 ch<-1。此时,非常有意思的事情发生了:

640?wx_fmt=gif

G1并没有锁住channel,然后将数据放到缓存中,而是直接把数据从G1直接copy到了G2的栈中。这种方式非常的赞!在唤醒过程中,G2无需再获得channel的锁,然后从缓存中取数据。减少了内存的copy,提高了效率。

之后的事情显而易见:640?wx_fmt=gif

参考文献:

  • https://www.youtube.com/watch?v=KBZlN0izeiY

  • https://zhuanlan.zhihu.com/p/27917262

640?wx_fmt=png

GO 中国征稿啦!


自“Go中国  ” 公众号上线以来,因为扎实的干货(害羞)、前沿的解读(娇羞)、满满的福利一直深受 Gopher 们的喜爱,为了给大家带来更具实力的干货以及 Go 语项目开发经验,我们将开始对外征稿!


现在我们开始对外征稿啦!如果你有优秀的 Go 语言技术文章想要分享,热点的行业资讯需要报道等,欢迎联系在菜单栏回复“投稿”“合作”联系我们的小编进行投稿。


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值