channel 底层原理
buf
是有缓冲的channel所特有的结构,用来存储缓存数据。是个循环链表 (为啥是循环链表?普通数组不行吗,普通数组地址和容量固定更适合指定的空间。需要pop 掉元素,普通数组需要全部都前移)sendx
和recvx
用于记录buf
这个循环链表中的~发送或者接收的~indexlock
是个互斥锁。recvq
和sendq
分别是接收(<-channel)或者发送(channel <- xxx)的goroutine抽象出来的结构体(sudog)的队列。是个双向链表
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
}
创建channel
ch := make(chan int, 3)
实例化了 hchan 的结构体,返回ch指针
channel中发送send(ch <- xxx)和recv(<- ch)接收
使用 mutex 加锁操作,新进先出的队列
当channel缓存满后
发送满的时候
当队列已满的时候,G1正在运行,当再次send操作时,会主动调用GO的调度器,让G1等待,并让出M,同时G1也会被抽象成含有G1指针和send元素的sudog结构体保存到hchan的sendq
中等待被唤醒。
当G2 recv操作的时候,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,提高了效率。
nil channel
向nil channel发送数据,或者接收数据,都会导致panic