channel
作为 Go 核心的数据结构, Goroutine 之间的通信方式,Channel 是支撑 Go 语言高性能并发编程模型的重要结构,本节很多代码都是能看懂的,所以粘贴了一些源码解读,大家可以尝试理解一下,具体内容会介绍 Channel 数据结构和接发数据操作
数据结构
Go语言 Channel 在运行时使用runtime.hchan
结构体实现接收发送数据的功能,下面展示一下各个字段的含义
源码位置:runtime.hchan
type hchan struct {
qcount uint //chan的len
dataqsiz uint //chan的cap
buf unsafe.Pointer //底层循环数组(存储数据的地方)
elemsize uint16 //chan类型大小
closed uint32 //1关闭 0无关闭
elemtype *_type //chan类型元数据
sendx uint //标记下一个发送点的数组索引
recvx uint //标记下一个接受点的数组索引
recvq waitq //因为没有容量或者容量满了阻塞接受Goroutine
sendq waitq //因为没有容量或者容量满了阻塞发送Goroutine
lock mutex //发送接受操作加锁
}
其中 recvq 和 sendq 表示因为接受或者发送阻塞的Goroutine,他们是一个双向链表结构
runtime.waitq
// 双向链表 链表的每个节点都是sudog,sudog表示等待的Goroutine
type waitq struct {
first *sudog
last *sudog
}
sudog结构表示存储等待Goroutine的信息
runtime.sudog
初始化
通过如下代码,我们来分析一下channel的初始化过程
ch := make(chan int)
没有指定channel的容量,就是默认容量为0,make关键字通过转化最后会通过runtime.makechan去初始化channel
func makechan(t *chantype, size int) *hchan {
elem := t.Elem
。。。。。省略代码
// 计算通道需要的内存大小
mem, overflow := math.MulUintptr(elem.Size_, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
var c *hchan
switch {
case mem == 0:
//如果当前 Channel 中不存在缓冲区,那么就只会分配一段内存空间大小为runtime.hchan内存对其后的大小
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case elem.PtrBytes == 0:
//这一行代码分配了 hchan 结构体和缓冲区所需的内存。
//mallocgc函数用于分配内存,并且通过 (hchanSize+mem) 指定了需要的总内存大小,即 hchan 结构体的大小加上缓冲区大小
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
//这一行代码设置了通道的 buf 字段,使其指向分配的内存块中 hchan 结构体之后的位置,即缓冲区的起始位置。
//unsafe.Pointer(c) 将 c 转换为指向 hchan 结构体的指针,
//然后通过 add 函数将其偏移 hchanSize 字节,从而得到缓冲区的起始位置
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
//在默认情况下会单独为 runtime.hchan 和缓冲区分配内存
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
c.elemsize = uint16(elem.Size_)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.Size_, "; dataqsiz=", size, "\n")
}
return c
}
- 当创建没有缓冲区的 Channel 的时候,那么就只会分配一段内存空间大小为
hchan
内存对其后的大小 - 当创建有缓存区并且channel的类型不为指针的时候,会计算一块连续的内存地址使用
- 其他情况,会单独申请
hchan
的内存大小和buf
的内存大小
最后会统一更新elemsize,elemtype,dataqsiz字段
发送数据
发送数据在代码编写做通过ch <- i
实现,主要逻辑在runtime.chansend函数里
在向channel发送数据的时候,如果channel已经关闭会直接panic
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
....
}
因为函数实现比较复杂,所以我们这里将函数分成以下的三个部分分析:
- 当存在等待接收Goroutine时,直接将数据发送给阻塞的Goroutine;
- 当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
- 当不存在缓冲区或者缓冲区已满时,挂起当前Goroutine,并保存Goroutine 到sendq中;
直接发送
如果目标 Channel 没有被关闭并且有接收等待的 Goroutine
,那么会从接收队列 recvq
中取出最先陷入等待的 Goroutine 直接向它发送数据
//直接发送数据给等待Goroutine
if sg := c.recvq.dequeue(); sg != nil {
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
....
if sg.elem != nil {
//将发送的数据直接拷贝到 x = <-c 表达式中变量 x 所在的内存地址上;
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
//唤醒Goroutine
goready(gp, skip+1)
}
runtime.send函数,会把发送的数据直接移动到接受 Goroutine 的 elem 字段的内存地址上
缓冲区
如果创建的 Channel 包含缓冲区并且 Channel 中的数据没有装满,会把数据发送到缓冲区中,然后channel的发送索引和长度加1
//缓存cap没有满
if c.qcount < c.dataqsiz {
//取出当前发送索引所在的位置
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
//移动数据到qp
typedmemmove(c.elemtype, qp, ep)
//发送索引++
c.sendx++
if c.sendx == c.dataqsiz {
c.sendx = 0
}
//channel长度++
c.qcount++
unlock(&c.lock)
return true
}
阻塞发送
当channel没有缓冲区或者缓冲区已满的情况,会创建一个 runtime.sudog
结构并将其加入 Channel 的 sendq
队列中,并且把当前 Goroutine 陷入阻塞等待唤醒
//获取发送数据使用的Goroutine
gp := getg()
//获取到sudog结构
mysg := acquireSudog()
mysg.releasetime = 0
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg
gp.param = nil
//添加当前g到sendq队列中
c.sendq.enqueue(mysg)
gp.parkingOnChan.Store(true)
//将当前的 Goroutine 陷入沉睡等待唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceBlockChanSend, 2)
KeepAlive(ep)
// someone woke us up.
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
//清空结构数值
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
mysg.c = nil
releaseSudog(mysg)
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
总结
向 Channel 里面发送数据流程图
接受数据
Go 语言中使用两种不同的方式去接收 Channel 中的数据:
i <- ch
i, ok <- ch
两种方式最后都会使用 runtime.chanrecv去接收数据
如果当前 Channel 已经被关闭并且缓冲区中不存在任何数据,那么会清除 ep
指针中的数据并立刻返回
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
....
lock(&c.lock)
if c.closed != 0 {
if c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// The channel has been closed, but the channel's buffer have data.
} else {
....
从 Channel 接收数据时还包含以下三种不同情况:
- 存在等待发送Goroutine时,通过runtime.recv获取数据;
- 当缓冲区存在数据时,从缓冲区中接收数据;
- 当缓冲区中不存在数据或者没有缓冲区时,挂起当前Goroutine,并保存挂起当前 Goroutine 信息到
recvq
中;
直接接收
当接收数据,存在等待发送 Goroutine 时:
- 如果接收的 Channel 没有缓冲区,就直接从发送 Goroutine 中接收数据;
- 如果有缓冲区,会接收recvx索引点数据,并把发送Goroutine的数据移动到recvx索引点,
因为要遵守先进先出的原则
;
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if c.dataqsiz == 0 {
if raceenabled {
racesync(c, sg)
}
if ep != nil {
//如果没有缓冲区,直接从G中copy数据
recvDirect(c.elemtype, sg, ep)
}
} else {
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
}
//获取c.recvx索引点的数据
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
//移动G的中数据到c.recvx索引点
typedmemmove(c.elemtype, qp, sg.elem)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
//清空G的数据
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
//唤醒G
goready(gp, skip+1)
}
缓冲区
当 Channel 的缓冲区中有数据,直接从缓冲区中 recvx
的索引位置中取出数据进行处理
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
...
if c.qcount > 0 {
//取出数据
qp := chanbuf(c, c.recvx)
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
typedmemclr(c.elemtype, qp)
c.recvx++
//等同于c.recvx %= c.dataqsiz
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount--
return true, true
}
...
}
阻塞接收
当 Channel 的发送队列中不存在等待的 Goroutine 并且缓冲区中也不存在任何数据时,阻塞当前的Goroutine,并把信息写入recvq,代码内容跟发送类似
总结
接收流程图
关闭通道
编译器会将用于关闭管道的 close
关键字转换成 runtime.closechan
函数。
当 Channel 是一个空指针
或者已经被关闭
时,Go 语言运行时会直接panic:
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
然后会closechan函数
将 recvq
和 sendq
两个队列中的数据加入到 Goroutine 列表 gList
中,与此同时该函数会清除所有 sudog
上未被处理的元素:
c.closed = 1
var glist gList
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
gp := sg.g
gp.param = nil
glist.push(gp)
}
for {
sg := c.sendq.dequeue()
...
}
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
最后会唤起所有被阻塞的 Goroutine。