为什么看channel源码
使用golang半年多,平时主要忙于用golang写业务逻辑。对golang多协程和channel等优雅的特性接触的比较少。最近难得有时间不用搬砖,因此通过读源码了解下在使用这些特性的时候底层(源码级别)到底发生了什么。
channel概述
当前对协程的理解是样的,它具有如下功能:
- 协程之间的同步控制,通过channel实现协程之间的通信,进而根据实现协程之间的同步。从这个角度来说channel内部的数据是担当信号的角色。
- 管道,channel内部的数据可以在各个协程中被加工处理,类似流水线操作。从这个角度来说channel是一个载体,使得数据可以在各个协程中有序流转。
细节分析
channel对应的数据结构主要包含环形队列、发送协程等待链表、接收协程等待链表组成。 其中环形队列用来存储想channel中写入的数据,等待队列放的是因读/写channel被堵塞的协程。 这里需要注意的一点是协程和channel之间的关系是多对多的关系,即一个协程可能读写多个channel,一个channel也可能被多个协程读写。
channel源码级别对应结构体如下:
type hchan struct {
qcount uint // 环形队列实际元素个数
dataqsiz uint // 环形队列大小
buf unsafe.Pointer // 环形队列地址
elemsize uint16 // 环形队列 元素大小
closed uint32 // 队列是否已经关闭,如果已经关闭则不能继续向队列写数据
elemtype *_type // 环形队列元素类型
sendx uint // 环形队列读的位置
recvx uint // 环形队列写的位置
recvq waitq // 因读channel堵塞的协程列表
sendq waitq // 因写channel堵塞的协程列表
lock mutex // 锁
}
当环形队列已满并且有协程因写环形队列而堵塞时,此时会取等待队列的协程将其置为就绪态。当环形队列为空时,此时会将channel放入等待队列。
channel创建
主要是对环形数组大小的合法性进行了检查,创建相应的环形数组。这里需要注意的是针对于环形数组中元素是指针和非指针这两种情况分开讨论。使用了不同的申请内存创建hchanel的方法。
case elem.kind&kindNoPointers != 0:
// 数组元素不包含指针时
c = (*hchan)(mallocgc(hchanSize+uintptr(size)*elem.size, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 元素包含指针时.
c = new(hchan)
c.buf = mallocgc(uintptr(size)*elem.size, elem, true)
为什么要区别对待? 貌似和垃圾回收有关系。
队列中元素不包含指针时,申请了一大块内存空间,环形队列也在这块内存空间中hchan后面,并且具体位置是位于。如果数组元素包含指针则环形队列单独一个内存空间。为什么这样做?
channel读操作
从channel读数据
x <- c 从chan c中读数据并把读出的元素放到x中
对应方法调用为:
// entry points for <- c from compiled code
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
channel写操作
向channel写数据
c <- x //向chan c写元素x
对应方法调用为:
// entry point for c <- x from compiled code
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
编译器完成从操作符到方法的转换??
如果等待队列中已经有协程因读channel数据导致堵塞,说明此时channel为空。此时写channel的协程不需要将数据写到环形队列,而是将数据直接复制给等待的读协程,raceenabled字段怎么用
if sg := c.recvq.dequeue(); sg != nil {
// Found a waiting receiver. We pass the value we want to send
// directly to the receiver, bypassing the channel buffer (if any).
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
并将该等待的协程由睡眠态改为就绪态。 在这个地方有没有涉及到调度器的重新调度,貌似没有。
channel close操作
主要是将所有因为读写该channel而堵塞的协程由睡眠态转为就绪态。源码如下:
// release all readers
for {
sg := c.recvq.dequeue()
...
glist = gp
}
// release all writers (they will panic)
for {
sg := c.sendq.dequeue()
...
gp.schedlink.set(glist)
glist = gp
}
...
// Ready all Gs now that we've dropped the channel lock.
for glist != nil {
gp := glist
glist = glist.schedlink.ptr()
gp.schedlink = 0
goready(gp, 3)
}
总结
- channel的读写方法基本是对称的
- 当前博客只是描述了一个大概的流程,很多细节暂时还未搞清楚,比如和gc相关的一些操作,比如goready操作的细节