本文章仅供个人学习使用。
参考
底层实现
数据结构
- Go的channel就是直接在堆上创建一个hchan,make会返回一个指向hchan的指针。
- hchan有几个核心字段,分别是:一把锁、一个环形缓冲区、环形缓冲区的两个指针sendx和recvx、两个队列sendq和recvq。
完整字段如下:
type hchan struct {
qcount uint // 循环队列中的数据总数
dataqsiz uint // 循环队列大小
buf unsafe.Pointer // 指向循环队列的指针
elemsize uint16 // 循环队列中的每个元素的大小
closed uint32 // 标记位,标记channel是否关闭
elemtype *_type // 循环队列中的元素类型
sendx uint // 已发送元素在循环队列中的索引位置
recvx uint // 已接收元素在循环队列中的索引位置
recvq waitq // 等待从channel接收消息的sudog队列
sendq waitq // 等待向channel写入消息的sudog队列
lock mutex // 互斥锁,对channel的数据读写操作加锁,保证并发安全
}
- sendq 和 recvq 是 sudog组成的双向链表:
type waitq struct {
first *sudog // sudog队列的队头指针
last *sudog // sudog队列的队尾指针
}
sudog 的结构是这样:
type sudog struct {
g *g // 绑定的goroutine
next *sudog // 指向sudog链表中的下一个节点
prev *sudog // 指向sudog链表中的下前一个节点
elem unsafe.Pointer // 数据对象
acquiretime int64
releasetime int64
ticket uint32
isSelect bool
success bool
parent *sudog // semaRoot binary tree
waitlink *sudog // g.waiting list or semaRoot
waittail *sudog // semaRoot
c *hchan // channel
}
channel 的初始化
初始化函数有一个参数是初始化的类型,分三种:
- 无缓冲区的chan,则直接在堆上为hchan结构体分配内存。
- 有缓冲区 && 元素类型不含指针,则直接在堆上为 hchan + buf 分配一段连续的内存。
- 有缓冲区 && 元素类型包含指针,则分配两次内存,先给hchan分配内存,再给buf数组分配内存。
func makechan(t *chantype, size int) *hchan {
elem := t.elem
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
// 如果内存超了,或者分配的内存大于channel最大分配内存,或者分配的size小于0,直接Panic
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
var c *hchan
switch {
case mem == 0:
// 没有缓冲区buf,只分配hchan这个结构的内存,不分配buf的内存
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0: // 有缓冲区buf,元素类型不含指针,为当前的 hchan结构和buf数组分配一块连续的内存空间
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// 有缓冲区,且元素包含指针类型,hchan结构和buf数组各自分配内存,分两次分配内存
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中写入
你看发送和接收函数,都有一个参数叫block,标识了是阻塞模式还是非阻塞模式。
- 阻塞模式是指:正常使用chan,比如 <- ch
- 非阻塞模式是指:select 多路复用。
首先判断当前chan是否为nil,为nil,则调用gopark()阻塞。
加锁。
判断chan是否已经close,如果是,则panic。
向channel中写入分3种情况:
- 如果有阻塞等待的接收者,则不经过缓冲区,直接把待发送的数据copy到接收处。
- 没有阻塞等待的接收者,则判断缓冲区是否有空闲空间,如果有,则把数据写入缓冲区,并更新
sendx和qcount. - 如果缓冲区已经满了,则将当前的groutine和要发送的数据封装成一个
sudog,将sudog加入到sendq,最后调用gopark将当前goroutine挂起(阻塞)。
解锁。
源码如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
if c == nil { // channel=nil,当前goroutine会被挂起
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
if debugChan {
print("chansend: chan=", c, "\n")
}
if raceenabled {
racereadpc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(chansend))
}
// 非阻塞,channel未关闭且channel是非缓冲型,并且等待接收队列为空;或者缓冲型,并且循环数组已经满了
if !block && c.closed == 0 && full(c) {
return false
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
// 加锁,控制并发
lock(&c.lock)
// 管道已经关闭,向关闭的channel发送数据,直接panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 接收队列非空,直接操作两个goroutine
// 什么意思? 就是当前 channel 有正在阻塞等待的接收方,就是之将数据由一个goroutine发往另一个goroutine
// 直接将待发送数据直接copy到接收处
// 直接从一个用一个goroutine操作另一个goroutine的栈
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必然有缓冲区
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
qp := chanbuf(c, c.sendx) // 将元素放在sendx处
if raceenabled {
racenotify(c, c.sendx, nil)
}
typedmemmove(c.elemtype, qp, ep)
c.sendx++ // sendx加1
if c.sendx == c.dataqsiz {
c.sendx = 0
}
c.qcount++ // channel总量加1
unlock(&c.lock)
return true
}
// 走到这里,说明上述情况为命中,channel已经满了,如果是非阻塞的直接返回,否则需要调用gopack将这个goroutine挂起,等待被唤醒
if !block {
unlock(&c.lock)
return false
}
gp := getg() // 获取发送数据的goroutine
mysg := acquireSudog() // 获取sudog 结构
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep // 设置待发送数据的内存地址
mysg.waitlink = nil
mysg.g = gp // 绑定发送goroutine
mysg.isSelect = false
mysg.c = c
gp.waiting = mysg // 设置到发送goroutine的waiting上
gp.param = nil
c.sendq.enqueue(mysg) // 将mysg这个sudog加入到当前channel的发送等待队列,等待被唤醒
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 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中读取
首先判断chan是否为nil,为nil则会阻塞,调用gopark().
加锁。
- 判断当前是否有发送者等待发送,如果有:
- 无缓冲区:直接将发送者唤醒,发送者将发送者的数据copy到接收者。
- 有缓冲区:缓冲区肯定已经满了。这时候将缓冲区中recvx指针处的数据读出,sendx和recvx都+1,唤醒一个发送者将数据发送到缓冲区。
- 如果当前没有发送者等待,则尝试从缓冲区中读取。检查
qcount > 0缓冲区不为空,则读取buf[recvx]处的数据, 并更新recvx和qcount。 - 如果缓冲区为空,则把当前的goroutine封装成sudog,把sudog加入到recvq队列,最后调用gopark阻塞当前goroutine。
解锁。
源码如下:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if debugChan {
print("chanrecv: chan=", c, "\n")
}
// channel是nil
if c == nil {
// 如果是非阻塞模式,直接返回false,false
if !block {
return
}
// 如果是阻塞模式,调用goprak挂起goroutine,等待被唤醒
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 在非阻塞模式下
// 如果是非缓冲型channel并且当前channel的等待发送链表为空或者是缓冲型channel并且buf中没有数据
if !block && empty(c) {
// 如果chan没有关闭,则返回 false, false
if atomic.Load(&c.closed) == 0 {
return
}
// 如果channel关闭了,双重检查,看channel是不是无缓冲chan或者是chan中没有数据,如果是则返回 true, false
if empty(c) {
if raceenabled {
raceacquire(c.raceaddr())
}
// 清除ep指针中的数据并立刻返回true,false
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
}
var t0 int64
if blockprofilerate > 0 {
t0 = cputicks()
}
lock(&c.lock)
// 如果channel已经关闭,并且chan中没有数据,返回 (true,false)
if c.closed != 0 && c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
// // 清除ep指针中的数据并立刻返回true,false
if ep != nil {
typedmemclr(c.elemtype, ep)
}
return true, false
}
// 优先从发送队列中取数据,如果有等待发送数据的groutine,直接从发送数据的goroutine中取出数据
if sg := c.sendq.dequeue(); sg != nil {
// 从当前channel的发送队列对头取出goroutine,说明有等待发送的goroutine
// 查看recv发现这里有两种情况
// 1. 如果是非缓冲型channel,那么直接将数据从发送者的栈copy到接收者的栈接收区
// 2. 如果是缓冲型channel,但是buf已经满了,首先将recvx处的元素拷贝到接收地址,然后将下一个写入元素拷贝到recvx,recvx和sendx都自增1
// 拷贝完数据以后,唤醒发送队列中的的goroutine,等待调度器调度
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
// 没有等待发送的队列,并且buf中有元素,从channel的缓冲区中接收数据
if c.qcount > 0 {
// 直接从缓冲区buf取出数据
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
// 将数据放到目标地址
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清空缓冲队列buf中对应的元素
typedmemclr(c.elemtype, qp)
c.recvx++ // 接收索引自增1
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.qcount-- // 队列元素数量减1
unlock(&c.lock)
return true, true
}
// 同步非阻塞模式,直接返回false,false
if !block {
unlock(&c.lock)
return false, false
}
// 走到这里说明是阻塞模式
// 没有任何数据可以获取到,阻塞住当前读goroutine,并加入channel的接收队列中
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// No stack splits between assigning elem and enqueuing mysg
// on gp.waiting where copystack can find it.
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
c.recvq.enqueue(mysg) // 加入到接收者队列
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
gp.waiting = nil
gp.activeStackChans = false
if mysg.releasetime > 0 {
blockevent(mysg.releasetime-t0, 2)
}
success := mysg.success
gp.param = nil
mysg.c = nil
releaseSudog(mysg) // 阻塞的goroutine被唤醒
return true, success
}
已经关闭的channel
规则
- 向其中写入数据会panic
- 从中读取数据 && 缓冲区不为空,可以正常读取。
- 从中读取数据 && 缓冲区为空,会返回该类型数据的零值(而不是阻塞)。此时,第二个返回值会返回false。
- 正确的读已经关闭的缓冲区的方式是for range 读取:
for v := range ch {
fmt.Println(v) // 会自动退出循环,当 ch 关闭且读空
}
- 如果channel为nil,关闭会发生panic。
- 如果关闭一个已经关闭的channel,会发生panic。
代码
package main
import "fmt"
func main() {
ch := make(chan int, 2)
ch <- 10
ch <- 20
close(ch)
// 还能读出缓冲区里的 10 和 20
fmt.Println(<-ch) // 10
fmt.Println(<-ch) // 20
// 缓冲区空了,再读会返回零值
fmt.Println(<-ch) // 0
// 用 v, ok 判断
v, ok := <-ch
fmt.Println(v, ok) // 0 false
}
运行结果:
10
20
0
0 false