golang 在多协程之间交互,除了支持传统的通过共享数据来进行通信(公共数据加锁),更推崇通过通信来共享数据。所谓的通过通信来共享数据,借鉴于csp并发模型(两个独立的并发实体通过共享的通讯管道进行通信的并发模型),即通过共享的channel在多个goroutine之间传递数据,达到共享数据的需求。
这里主要谈谈channel是如何实现的,先看看源码:
type hchan struct {
qcount uint // 队列中的元素个数
dataqsiz uint // 循环队列的数据大小
buf unsafe.Pointer // 缓存数组指针
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // 待发送的index
recvx uint // 待接收的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
}
type waitq struct {
first *sudog
last *sudog
}
注意,make(chan string),返回的是一个hchan的指针,所以在多个goroutine之间共享的是一个地址。
hchan结构体使用一个循环队列来保存groutine之间传递的数据,使用两个list来保存往该chan发送和从该chan接收数据的goroutine,还有一个mutex来保证操作这些结构的安全。
初始的时候hchan结构体的buf为空,sendx和recvx都为0,当向chan里发送数据的时候,会首先对buf加锁,然后将要发送的数据copy到buf,并增加sendx的值,最后释放buf的锁。然后读取chan的时候首先对buf加锁,然后将buf里的数据copy到待接收的变量里,增加recvx,最后释放锁。整个过程,多个goroutine之间没有共享的内存,底层通过hchan结构体的buf,使用copy内存的方式进行通信,最后达到了共享内存的目的。
在hchan中的recvq,sendq又是做什么用的呢,recvq是由 recv 行为(也就是 <-ch)阻塞在 channel 上的 goroutine 队列,sendq是由 send 行为 (也就是 ch<-) 阻塞在 channel 上的 goroutine 队列。 这个队列(waitq)里面存的是sudog结构,sudog其实就是一个对 g 的结构的封装。
当向chan写数据的时候发生阻塞的时候又发生了什么呢?
当向已经满了的ch发送数据的时候,runtine检测到对应的hchan的buf已经满了,会通知调度器,调度器会将goroutine的状态设置为waiting, 移除与线程M的联系,然后从P的runqueue中选择一个goroutine在线程M中执行,此时这个goroutine就是阻塞着的。d当这个goroutine变为waiting状态后,会创建一个代表自己的sudog的结构,然后放到sendq这个list中,sudog结构中保存channel相关的变量的指针(如果该Goroutine是sender,那么保存的是待发送数据的变量的地址,如果是receiver则为接收数据的变量的地址,之所以是地址,前面我们提到在传输数据的时候使用的是copy的方式),当另一个goroutine从ch中接收一个数据时,会通知调度器,设置先前阻塞的goroutine的状态为runnable,然后将加入P的runqueue里,等待线程执行。
当向chan读数据的时候发生阻塞的时候又发生了什么呢?
和前面介绍的写阻塞一样,读阻塞也会创建一个sudog结构体,保存接收数据的变量的地址,该sudog结构体是放到了recvq列表里,当其他goroutine向ch发送数据的时候,runtime并没有对hchan结构体题的buf进行加锁,而是直接将要发送到ch的数据copy到了读阻塞sudog里对应的elem指向的内存地址!