Golang channel 之 数据结构
channel的作用
channel被设计用来实现goroutine间的通信,按照golang的设计思想:以通信的方式共享内存。
channel的内存布局
例如如下代码中的make函数会在堆上分配一个runtime.hchan类型的数据结构,ch是存在于函数f栈帧上的一个指针,指向堆上的hchan数据结构。
func f() { ch := make(chan int) ... }
至于为什么是堆上的一个结构体:首先,要实现channel这样的复杂功能,肯定不是几个字节可以搞定的,所以需要一个struct来实现;其次,这种被设计用来实现协程间通信的组件,其作用域和生命周期不可能仅限于某个函数内部,所以golang直接将其分配在堆上。
channel的数据结构
下面结合在channel中的作用,解读一下hchan中都有哪些字段:
1)协程间通信肯定涉及到并发访问,所以要有锁来保护整个数据结构:
type hchan struct { ... lock mutex }
2)channel分为“无缓冲”和“有缓冲”两种,对于有缓冲channel来讲,需要有相应的内存来存储数据,实际上就是一个数组,需要知道数组的地址、容量、元素的大小,以及数组的长度也就是已有元素个数,加上这几个字段后,上面的结构体就变成了这样:
type hchan struct { qcount uint // 数组长度,即已有元素个数 dataqsiz uint // 数组容量,即可容纳元素个数 buf unsafe.Pointer // 数组地址 elemsize uint16 // 元素大小 ... }
3)因为golang运行时中内存复制、垃圾回收等机制依赖数据的类型信息,所以hchan中还要有一个指针,指向元素类型的类型元数据:
type hchan struct { ... elemtype *_type // 元素类型 ... }
4)channel支持交替的读写(称send为写,recv为读,更简洁),有缓冲channel内的缓冲数组会被作为一个“环型”来使用,当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置:
type hchan struct { ... sendx uint // 下一次写下标位置 recvx uint // 下一次读下标位置 ... }
5)当读和写请求不能立即被满足时,需要能够让当前协程在channel上等待,当请求能够被满足时,要能够立即唤醒等待的协程,所以要有两个等待队列,分别针对读和写:
type hchan struct { ... recvq waitq // 读等待队列 sendq waitq // 写等待队列 ... }
6)channel是能够被close的,所以要有一个字段记录是否已经close掉了:
type hchan struct { ... closed uint32 ... }
最后整合起来,runtime.hchan结构是这个样子:
type hchan struct { qcount uint // 数组长度,即已有元素个数 dataqsiz uint // 数组容量,即可容纳元素个数 buf unsafe.Pointer // 数组地址 elemsize uint16 // 元素大小 closed uint32 elemtype *_type // 元素类型 sendx uint // 下一次写下标位置 recvx uint // 下一次读下标位置 recvq waitq // 读等待队列 sendq waitq // 写等待队列 lock mutex }
本篇先到这里,至于channel的读写操作和select机制,留到后面的文章中解读。