golang channel底层结构和实现

一、介绍Golang 设计模式: 不要通过共享内存来通信,而要通过通信实现内存共享
channel是基于通信顺序模型(communication sequential processes, CSP)的并发模式,可以让一个 goroutine 发送特定值到另一个 goroutine 的通信机制
channel中的数据遵循先入先出(First In First Out)的规则,保证收发数据的顺序
二、结构channel的源码在runtime包下的chan.go文件, 参见chan.go
以下时channel的部分结构:
type hchan struct { qcountuint dataqsiz uint bufunsafe.Pointer elemsize uint16 closeduint32 elemtype *_type sendxuint recvxuint recvqwaitq sendqwaitq lock mutex}type waitq struct { first *sudog last*sudog}其中:
qcount: 队列中剩余的元素个数dataqsiz: 环形队列长度,即可以存放的元素个数, make初始化时指定buf: 缓存区,实际上就是环形队列(有环形队列就有缓冲区,否则没有缓冲区),指向环形队列首部的指针,基于环形队列实现,大小等于make初始化channel时指定的环形队列长度,如果make初始化channel时不指定dataqsiz,则buf=0 。只有缓冲型的channel才有bufelemsize: 每个元素的大小closed: channel关闭标志elemtype: 元素类型sendx: 写入数据的索引,即从哪个位置开始写入数据,取值[0, dataqsiz)recvx: 读取数据的索引,即从哪个位置开始读取数据,取值[0, dataqsiz)recvq: 接收等待队列,链表结构,长度无限长, 读取数据的goroutine等待队列, 如果channel的缓冲区为空或者没有缓冲区,读取数据的goroutine被阻塞,加入到recvq等待队列中 。因读阻塞的goroutine会被向channel写入数据的goroutine唤醒sendq: 发送等待队列,链表结构,长度无限长, 写入数据的goroutine等待队列, 如果channel的缓冲区为满或者没有缓冲区,写入数据的goroutine被阻塞,加入到sendq等待队列中 。因写阻塞的goroutine会被从channel读取数据的goroutine唤醒lock: 并发控制锁, 同一时刻,只允许一个, channel不允许并发读写
1 结构图

golang channel底层结构和实现

文章插图
其中:
环形队列中的0表示没有数据,1表示有数据; G表示一个goroutinedataqsiz表示环形队列的长度为6, 即可缓存6个元素buf指向环形队列首部,此时还可以缓存2个元素qcount表示环形队列中有4个元素sendx表示下一个发送的数据在环形队列index=5的位置写入,取值[0, 6)recvx表示从环形队列index=1的位置读取数据,取值[0, 6)sendq, recvq: 虚线表示,此时转态下的channel可能有等待队列三、channel的创建1 声明channel类型
//同时读写的channelvar 变量 chan 类型//只能写入数据的channelvar 变量 chan<- 类型//只能读取数据的channelvar 变量 <-chan 类型 其中:
类型:channel内的数据类型,golang支持的合法类型
声明的channel此时还是nil,需要配合make函数初始化之后才能使用
2 创建channel
//无缓冲的channel变量 := make(chan 数据类型)//有缓冲的channel变量 := make(chan 数据类型, dataqsiz)
四、向channel发送数据1 发送数据的格式
变量 <- 值2 写数据的过程
1) 流程图如下:
golang channel底层结构和实现

文章插图
 
其中:
G表示一个goroutine虚线表示sendq中堵塞的G被唤醒的流程,如果G没有被唤醒,则一直堵塞下去,此时关闭channel,会触发panic2) 过程描述:
1) 如果channel是nil(没有初始化), 发送数据则一直会堵塞,这是一个BUG2) 如果等待接收队列recvq 不为空,说明没有缓冲区或者缓冲区没有数据,直接从recvq取出一个G数据写入,把G唤醒,结束发送过程3) 如果等待接收队列recvq为空,且缓冲区有空位,那么就直接将数据写入缓冲区sendx位置, sendx++, qcount++, 结束发送过程4) 如果等待接收队列recvq为空,缓冲区没有空位,将数据写入G,然后把G放到等待发送队列sendq中进行阻塞,等待被唤醒, 结束发送过程 。当被唤醒的时候,需要写入的数据已经被读取出来,且已经完成了写入操作五、从channel接收数据1 接收数据的格式
1) 阻塞接收数据
程序阻塞直到收到数据并赋值
data := <-ch
2) 非阻塞接收数据
非阻塞的通道接收方法可能造成高的 CPU 占用
//ok表示是否接收到数据data, ok := <-ch
3) 接收数据并忽略
程序阻塞直到接收到数据,但接收到的数据会被忽略
<-ch
4) 循环接收
channel是可以进行遍历的,遍历的结果就是接收到的数据
for data := range ch {//done}5) SELECT语句接收
select 的特点是只要其中有一个 case 已经完成,程序就会继续往下执行,而不会考虑其他 case 的情况在一个 select 语句中,Go语言会按顺序从头至尾评估每一个发送和接收的语如果其中的多条case语句可继续执行(即没有被阻塞),那么就从这些case语句中任意选择一条如果没有case语句可以执行(即所有的通道都被阻塞):1) 如果有 default 语句,执行 default 语句,同时程序的执行会从 select 语句后的语句中恢复2) 如果没有 default 语句,那么 select 语句将被阻塞,直到至少有一个case可以进行下去

推荐阅读