废话很少说,直奔主题。git
简单说明:github
buf
是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表sendx
和recvx
用于记录buf
这个循环链表中的~发送或者接收的~indexlock
是个互斥锁。recvq
和sendq
分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表源码位于/runtime/chan.go
中(目前版本:1.11)。结构体为hchan
。golang
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。bash
ch := make(chan int, 3) 复制代码
建立channel实际上就是在内存中实例化了一个hchan
的结构体,并返回一个ch指针,咱们使用过程当中channel在函数之间的传递都是用的这个指针,这就是为何函数传递中无需使用channel的指针,而直接用channel就好了,由于channel自己就是一个指针。微信
先考虑一个问题,若是你想让goroutine以先进先出(FIFO)的方式进入一个结构体中,你会怎么操做? 加锁!对的!channel就是用了一个锁。hchan自己包含一个互斥锁mutex
markdown
channel中有个缓存buf,是用来缓存数据的(假如实例化了带缓存的channel的话)队列。咱们先来看看是如何实现“队列”的。 仍是刚才建立的那个channel函数
ch := make(chan int, 3) 复制代码
当使用send (ch <- xx)
或者recv ( <-ch)
的时候,首先要锁住hchan
这个结构体。oop
而后开始send (ch <- xx)
数据。 一ui
ch <- 1 复制代码
二
ch <- 1 复制代码
三
ch <- 1 复制代码
这时候满了,队列塞不进去了 动态图表示为:
而后是取recv ( <-ch)
的过程,是个逆向的操做,也是须要加锁。
而后开始recv (<-ch)
数据。 一
<-ch
复制代码
二
<-ch
复制代码
三
<-ch
复制代码
图为:
注意以上两幅图中buf
和recvx
以及sendx
的变化,recvx
和sendx
是根据循环链表buf
的变更而改变的。 至于为何channel会使用循环链表做为缓存结构,我我的认为是在缓存列表在动态的send
和recv
过程当中,定位当前send
或者recvx
的位置、选择send
的和recvx
的位置比较方便吧,只要顺着链表顺序一直旋转操做就好。
缓存中按链表顺序存放,取数据的时候按链表顺序读取,符合FIFO的原则。
注意:缓存链表中以上每一步的操做,都是须要加锁操做的!
每一步的操做的细节能够细化为:
每一步的操做总结为动态图为:(发送过程)
或者为:(接收过程)
因此不难看出,Go中那句经典的话:Do not communicate by sharing memory; instead, share memory by communicating.
的具体实现就是利用channel把数据从一端copy到了另外一端! 还真是符合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 复制代码
这个时候G1正在正常运行,当再次进行send操做(ch<-1)的时候,会主动调用Go的调度器,让G1等待,并从让出M,让其余G去使用
同时G1也会被抽象成含有G1指针和send元素的sudog
结构体保存到hchan的sendq
中等待被唤醒。
那么,G1何时被唤醒呢?这个时候G2隆重登场。
G2执行了recv操做p := <-ch
,因而会发生如下的操做:
G2从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,而后调用Go的scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中。
你可能会顺着以上的思路反推。首先:
这个时候G2会主动调用Go的调度器,让G2等待,并从让出M,让其余G去使用。 G2还会被抽象成含有G2指针和recv空元素的sudog
结构体保存到hchan的recvq
中等待被唤醒
此时刚好有个goroutine G1开始向channel中推送数据 ch <- 1
。 此时,很是有意思的事情发生了:
G1并无锁住channel,而后将数据放到缓存中,而是直接把数据从G1直接copy到了G2的栈中。 这种方式很是的赞!在唤醒过程当中,G2无需再得到channel的锁,而后从缓存中取数据。减小了内存的copy,提升了效率。
以后的事情显而易见:
互联网技术窝
或者加微信共同探讨交流:参考文献: