这期介绍关于golang channel的内部工作原理,从源码入手结合资料介绍channel作为goroutine的通信机制创建,写入,读出的过程。文中截图均源自或改编自kavya的understanding channels. 需要基本的goroutine知识,以及channel使用方法为基础来理解。
在golang语言设计中,goroutine可以独立并行的执行任务,channel负责goroutine之间的交互和同步。
channel内部由一个hchan的数据结构构成:
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 mutex
}
除了各种容量参数外,buf是一个指向在堆中初始化的内存的指针,代表一个存储元素的队列;
elemtype是我们初始化channel存放的类型
sendq和recvq分别是存储发送和等待goroutine task的队列,内部由sudog栈来维护
sendx,recvx记录发送和接收的索引
lock为互斥锁保证channel读写的线程安全
在hchan数据结构中我们看到,channel中是有buf数据结构的,这反映在我们make channel的方式中:
ch = make(chan int) // unbuffered channel ch = make(chan int, 0) // unbuffered channel ch = make(chan int, 3) // buffered channel with capacity 3
当make chan时其实是在堆中分配了一个hchan结构,并返回其指针指向堆地址空间以便于使用:
此时,channel中会分配使用hchan结构,我们看到hchan结构的buf是一个环形队列,需要sendx和recvx记录发送和接收的位置,并按照声明参数来初始化大小和存放数据的类型。:
当由goroutine向channel中写数据时,
1. 该goroutine获取hchan中的互斥锁
2. 把任务复制进到buf中的队列中,并sendx加一
3. 释放互斥锁
当有goroutine向channel中读出数据时,
1. 获取hchan结构中的锁
2. hchan中buf队列出队,并返回给goroutine
3. 释放hchan的锁
这种方式避免了进程间通信大量的共享内存,而是用hchan结构进行交互替代
这个时候产生问题:
如果是无缓存channel或者goroutine使用channel读写超过了buf初始容量,该怎么办?
这里讲到goroutine其实是更轻量的用户级线程,即被golang的运行期创建管理,而不是内核态线程。对应到内核态线程可能是m对n的形式,一个内核态线程对应管理多个goroutine的任务。内核态线程通过context去管理goroutine,保证可执行goroutine的序列。
其中M是操作系统的线程,G是用户启动的goroutine,P是与调度相关的context,每个M都拥有一个P,P维护了一个能够运行的goutine队列,用于该线程执行。
sendq队列中维护了sudog数据结构由栈组成,存放goroutine同步的必要信息和存放元素。goroutine等待状态时,操作scheduling context而不影响内核态线程。当buf中移出元素时buf队列出队,sudog栈pop出元素,buf进队列。
当sudog pop元素时,我们可以利用运行期栈空间质相邻特点把G1空间写到G2空间中,这样直接发送的方式避免了切换开销
这里buf是一段放数据用的内存,锁是用的结构体里的mutex,并且进入send函数之后就会上锁了,之后才会考虑是放入buf还是sudog
总结:buf是一个golang存放缓存任务的队列,当队列满时,会在sendq中创建sudog结构并存放在goroutine的栈空间内,当前goroutine状态改为等待并通过内核态的OS thread的scheduler context管理做到阻塞goroutine而不影响OS thread。当buf中任务被消费出现空缺,buf出队列,sudog中goroutine唤醒,并地址扩展到当前goroutine,buf进队列。