golang channel
”不要以共享内存的方式来通信,相反,要通过通信来共享内存“
golang 的一个思想,不整文的,整点武的,具体来看channel 怎么做的
有一个很关键的 golang MPG 模型再单独分析,这篇先只分析 channel
定义
// runtim/chan.go
type hchan struct {
qcount uint // 通道里的元素数量
dataqsiz uint // 通道的总大小
buf unsafe.Pointer // 通道底层循环数组的指针
elemsize uint16 // 每个元素的大小
closed uint32 // 通道是否关闭,不能重复关闭,会 panic
elemtype *_type // 通道里元素的类型
sendx uint // 通道里发送元素的下标
recvx uint // 接受元素的下标
recvq waitq // 准备接受的协程链表
sendq waitq // 准备发送的写成链表
lock mutex // 通道锁,chan 是并发安全的
}
链表,保存发送或接受协程
// runtim/chan.go
type waitq struct {
first *sudog
last *sudog
}
初始化
channel使用 make 初始化
c := make(chan int) // 无缓冲
c2 := make(chan int,10) // 有缓冲
当无缓冲时,直接创建,当有缓冲时,还要调用 mallocgc 函数,申请空间,具体代码可以看 runtime/chan.go
发送数据
使用来说很简单,下面的代码如果 channel 满了或者无缓冲无接受者的话就会阻塞
c <- 10
具体发送代码在 runtime/chan.go(chansend) 函数,就不贴代码了,直接分析流程
除了必要的一些检查之外,先对 channel 进行加锁,如果 channel 已经关闭了,就解锁退出
然后判断接收区有没有 goroutine 在等待,如果有的话,直接把数据拷贝给接收区的 goroutine,然后唤醒 goroutine
否则看缓冲区是否满了,如果没满,就把数据拷贝到缓冲区sendx处,然后 sendx++,qcount++,退出
如果缓冲区也满了,则把发送的 goroutine 放入到发送 sendq 队列
一个流程就结束了
接受数据
x := <- c
具体关闭代码在 runtime/chan.go(chanrecv)
先进行检查,判断 channel 是否是空的,如果是空的直接返回(判空条件是qcount == 0 并且sendq里没有 goroutine)
然后加锁
然后判断 channel 是否已经关闭,关闭的话直接返回变量类型的空值
如果缓冲区有数据,则直接拷贝缓冲区数据到接受,recvx++,qcount--
如果上述都不符合,则把接受goroutine 保存到接受队列
关闭通道
close(c)
源码在 runtime/chan.go(closechan)
先加锁,如果已经关闭了,则 panic
然后将 close 置为 1
把所有的接受队列填充空值并唤醒
把所有的发送队列唤醒,让他们 panic
各种情况处理
close 一个正常的 channel 会正常,否则都会 panic,也就是 close 只能正确的调用一次
发送一个 close 的 channel 会 panic,发送一个 nil 的 channel 会一直阻塞
接受一个 close 的 channel 会接受到 0 值,接受一个 nil 的 channel 会一直阻塞
参考
Go 程序员面试笔试宝典-通道
图解Golang channel源码
Go面试题(五):图解 Golang Channel 的底层原理