channel结构体
type hchan struct {
qcount uint //大小
dataqsiz uint //有缓存的队列大小
buf unsafe.Pointer //有缓存的循环队列指针
elemsize uint16
closed uint32
elemtype *_type //类型
sendx uint //有缓存的可发送下标
recvx uint //有缓存的可存储下标
recvq waitq //接受的goroutine抽象出来的结构体sudog的队列,是一个双向链表
sendq waitq //同上,是发送的相关链表
lock mutex //互斥锁
}
channel创建
ch := make(chan int,3)
创建channel实际上就是在内存中实例化了一个hchan的结构体,并返回一个ch指针,我们使用过程中channel在函数之间的传递都是用的这个指针,这就是为什么函数传递中无需使用channel的指针,而直接用channel就行了,因为channel本身就是一个指针。
channel发送和接收
go中的经典话语Do not communicate by sharing memory; instead, share memory by communicating.
说的事不要通过共享内存进行通信,而应该通过通信达到共享数据的目的
缓存未满或未空的情况
channel中的发送盒接收可以细化为下面三个步骤
- 加锁
- 把数据从goroutine中copy到队列(或者队列中copy到goroutine)
- 释放锁
缓存满或空的情况
情况一
当G1已经将channel的缓存存满后,当再次进行send操作ch<-1
的时候,会主动调用Go的调度器,让G1等待,并从让出M,让其他G去使用,同时G1也会被抽象成含有G1指针和send元素的sudog结构体保存到hchan的sendq中等待被唤醒。
当G2执行了recv操作p := <-ch
,于是G2从缓存队列中取出数据,channel会将等待队列中的G1推出,将G1当时send的数据推到缓存中,然后调用Go的scheduler,唤醒G1,并把G1放到可运行的Goroutine队列中。
情况二
当G2在channel的缓存空时,进行取操作,则G2会主动调用Go的调度器,让G2等待,并从让出M,让其他G去使用。G2还会被抽象成含有G2指针和recv空元素的sudog结构体保存到hchan的recvq中等待被唤醒
当G1开始向channel中推送数据ch <- 1
时,则G1并不会锁住channel,然后将数据放到缓存中,而是直接把数据从G1直接copy到了G2的栈中。这种方式在唤醒过程中,G2无需再获得channel的锁,然后从缓存中取数据。减少了内存的copy,提高了效率。