channel 是什么
顾名思义,channel就是一个通信管道,被设计用于实现goroutine之间的通信
Go语言尊崇的设计思想是:以通信的方式来共享内存,而不是通过共享内存来实现通信,channel就是这一思想的体现
下面就让我们走进channel的世界吧
channel 的数据结构
channel的功能比较复杂,所以就不会是几个字节就能实现的,所以需要一个复杂的struct来承接channel的作用,也就是下文的hchan结构体
然后要注意channel是直接分配到堆上的,因为channel从设计理念上看,就是用于goroutine之间的通信,作用域和生命周期不会被限制在一个函数中
runtime.hchan的类型定义在源码 src/runtime/chan.go中:
type hchan struct {
qcount uint // channel 环形数组中元素的数量
dataqsiz uint // channel 环形数组的容量
buf unsafe.Pointer // 指向channel 环形数组的一个指针
elemsize uint16 // 元素所占的字节数
closed uint32 // 是否关闭
elemtype *_type // 元素类型
sendx uint // send index 下一次写的位置
recvx uint // receive index 下一次读的位置
recvq waitq // list of recv waiters 读等待队列
sendq waitq // list of send waiters 写等待队列
lock mutex // runtime.mutex,保证channel并发安全
}
PS:上面图画错了,recvx表示下一次读,sendx表示下一次写
下面我们来理解一下hchan中的字段
对于channel,我们可以将数据缓存到其中,所以有一个buf数组,用来缓冲数据,又因为channel可以同时提供读写功能,所以我们有sendx 和 recvx分别指向下一次写和下一次读的位置,buf、sendx,recvx 就构成了一个环形数组,每次读或写超过最后一个下标,就会回到下标0处。
然后用qcount表示buf中元素数量,用dataqsiz表示buf的容量。
因为channel设计是用在多个goroutine之间的通信上的,所以需要一把mutex来保护读、写、关闭操作的并发安全
然后因为channel提供一个读和写等待队列(recvq和sendq)来帮助goroutine在未完成读写操作后,可以被阻塞挂起,然后等待channel通信来临时,再被唤醒调度
下面我们看一下recvq 和 sendq的结构,也就是waitq结构体,可以把waitq看作是一个链表构成的队列
type waitq struct {
first *sudog // sudog队列的队头指针
last *sudog // sudog队列的队尾指针
}
然后介绍一下sudog这个结构体,sudog可以看作是对阻塞挂起的g的一个封装,然后用多个sudog来构成等待队列
下面看一下sudog结构(只留下主要字段):
type sudog struct {
g *g // 绑定的goroutine
next *sudog // 前后指针
prev *sudog
elem unsafe.Pointer // 存储元素的容器
isSelect bool // 标识是不是因为select操作封装的sudog
// 为true,表示这个sudog是因为channel通信唤醒的
// 否则为false,表示这个sudog是因为channel close唤醒的
success bool
c *hchan // 绑定的channel
}
这里关注下elem字段,elem作为收发数据的容器
当向channel发送数据时,elem代表将要写进channel的元素地址
当从channel读取数据时,elem代表要从channel中读取的元素地址
channel操作
channel初始化
Go语言中,我们只能通过make函数来初始化一个channel,runtime会调用runtime.makechan函数来完成channel的初始化工作
源码位于src/runtime/chan.go中:
func makechan(t *chantype, size int) *hchan {
// channel 元素类型
elem := t.elem
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
var c *hchan
switch {
case mem == 0:
// channel无缓冲 or 元素大小为0,只需要分配一个hchan
c = (*hchan)(mallocgc(hchanSize, nil, true))
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// channel 元素不包含指针,hchan和buf 一起分配
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// channel 元素包含指针,hchan和buf 分开分配
// 因为 申请的span 分为scan 和 noscan,无法一起分配
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// channel 的一些初始化
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
return c
}
makechan函数有两个参数t *chantype, size int,第一个参数代表要创建的channel的元素类型,而第二个参数代表通道环形缓冲的容量大小
可以看出为channel开辟内存分为三种情况:
- channel 无缓冲 or 元素大小为0:只需要分配hchan本身结构体大小的 内存
- 有缓冲区buf, 但元素不包含指针:hchan和buf 一起分配
- 有缓冲区buf,且元素包含指针类型:hchan和buf 分开分配
针对channel的不同状态,向channel写入结果如下:
Channel 写入
下面是往channel中写入一个数据的例子
ch := make(chan int)
ch <- 1 // 往管道里写入1
底层其实就是调用了runtime.chansend函数,源码如下:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// c:对应的hchan指针
// ep:被发送到channel的变量的地址
// block:发送操作是否阻塞(代表如果send不能立即完成的话,是否阻塞)
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型,直接返回falase
if !block {
return false
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 非阻塞类型channel and channel没关闭 and channel满了
if !block && c.closed == 0 && full(c) {
return false
}
// 加锁
lock(&c.lock)
// channel关闭了,执行写操作,触发panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 尝试从读等待队列中取出一个goroutine
if sg := c.recvq.dequeue(); sg != nil {
// 读等待队列有goroutine,将写入数据直接交给对应的goroutine
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 如果环形数组还有容量可以写入
if c.qcount < c.dataqsiz {
// 通过sendx 找到写入位置的地址
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 将ep中的数据写入到qp中
typedmemmove(c.elemtype, qp, ep)
c.sendx++
// 如果sendx == dataqsiz,因为是缓存数组,如果将snedx置为0
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// c:对应的hchan指针
c.qcount++
unlock(&c.lock)
return true
}
// 非阻塞类型的写操作走到这一步,不管有没有写入到channel中,都不需要阻塞,直接return
if !block {
unlock(&c.lock)
return false
}
// Block on the channel. Some receiver will complete our operation for us.
gp := getg()
// 取出一个sudog结构
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 设置对应状态
mysg.elem = ep
mysg.waitlink = nil
// 绑定goroutine
mysg.g = gp
mysg.isSelect = false
// 绑定channel
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 进入写等待队列
c.sendq.enqueue(mysg)
gp.parkingOnChan.Store(true)
// gopark操作
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
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发送数据会出现三种情况
case1:channel中有读等待goroutine
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ........
// 加锁
lock(&c.lock)
// 尝试从读等待队列中取出一个goroutine
if sg := c.recvq.dequeue(); sg != nil {
// 读等待队列有goroutine,将写入数据直接交给对应的goroutine
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// ........
}
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 将ep 复制到 sg对应的elem上
if sg.elem != nil {
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)
}
- 先拿锁
- 从recvq(读等待队列)里面弹出队列头部的sudog,进入send流程
- 将要写入的数据拷贝得到这个sudog对应的elem数据容器上
- 释放锁
- 唤醒sudog绑定的goroutine(也就是将这个goroutine重新放入到gmp模型中,等待调度)
case2:channel中没有读等待goroutine,并且环形缓冲数组里面有剩余空间
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// ........
// 加锁
lock(&c.lock)
// 如果环形数组还有容量可以写入
if c.qcount < c.dataqsiz {
// 通过sendx 找到写入位置的地址
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 将ep中的数据写入到qp中
typedmemmove(c.elemtype, qp, ep)
c.sendx++
// 如果sendx == dataqsiz,因为是缓存数组,如果将snedx置为0
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// channel 中的元素数量+1
c.qcount++
unlock(&c.lock)
return true
}
// ..............
}
- 先拿锁
- 将数据写入到 sendx指向的位置中
- sendx++,qcount++
- 释放锁
case3:channel中没有读等待goroutine,并且无剩余空间存放数据
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// .......
gp := getg()
// 取出一个sudog结构
mysg := acquireSudog()
// 将ep存入到elem中
mysg.elem = ep
// 绑定goroutine
mysg.g = gp
// 绑定channel
mysg.c = c
// 进入写等待队列
c.sendq.enqueue(mysg)
// gopark操作
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// 处理状态
gp.waiting = nil
gp.activeStackChans = false
closed := !mysg.success
gp.param = nil
mysg.c = nil
// 回收sudog
releaseSudog(mysg)
return true
}
- 锁保护步骤同样有的
- 获取一个sudog结构,绑定对应的channel,goroutine,还有ep指针
- 将sudog放入channel的写等待队列(sendq)
- runtime.gopark(挂起当前goroutine,可以看作是解绑当前g和m,然后开启下一轮调度)
特殊case
第一种特殊情况:写入的channel为nil
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// c:对应的hchan指针
// ep:被发送到channel的变量的地址
// block:发送操作是否阻塞(代表如果send不能立即完成的话,是否阻塞)
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型,直接返回falase
if !block {
return false
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// ............
}
- 当channel为nil的时候,对channel进行写操作,会导致当前goroutine永久性挂起,如果当前goroutine是main goroutine的话,还会导致整个程序退出
第二种特殊情况:channel已经关闭,还想进行写操作
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 加锁
lock(&c.lock)
// channel关闭了,执行写操作,触发panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// ............
}
- 当channel已经被关闭,再向channel写数据,会出现panic
channel读取
从channel读取数据的编码形式如下
ch := make(chan, int)
v := <- ch // 直接读取
v, ok <- ch // ok判断读取的v是否有效
底层其实就是调用了runtime.chanrecv函数,源码如下:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// c:对应的hchan指针
// ep:被发送到channel的变量的地址
// block:发送操作是否阻塞(代表如果send不能立即完成的话,是否阻塞)
// selected和received
// 如果received为true,则说明数据是从channel接收到的
// 如果received为false,selected为true,说明channel是通道关闭,并且得到零值
// 如果received为false,selected为false,则是因为非阻塞操作返回
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型,直接返回
if !block {
// 两个false,通道不想阻塞而返回
return false, false
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// 非阻塞类型,并且channel是空的(无缓冲,且sendq为空)
if !block && empty(c) {
if atomic.Load(&c.closed) == 0 {
// 通道没有关闭,返回两个false,通道不想阻塞而返回
return
}
if empty(c) {
// 将ep清空
if ep != nil {
typedmemclr(c.elemtype, ep)
}
// 通道关闭了,但是channel是空的(无缓冲,且sendq为空),返回true,false,表示channel因为通道为空,收到零值
return true, false
}
}
// 加锁
lock(&c.lock)
// 如果channel已经关闭
if c.closed != 0 {
// 通道已经关闭,返回两个false,通道不想阻塞而返回
if c.qcount == 0 {
if raceenabled {
raceacquire(c.raceaddr())
}
unlock(&c.lock)
// 清空ep
if ep != nil {
typedmemclr(c.elemtype, ep)
}
// 返回false,false,说明channel关闭,并且得到零值
return true, false
}
} else {
// 如果通道没有关闭,并且写队列里面有 等待的goroutine
if sg := c.sendq.dequeue(); sg != nil {
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
}
// channel中有元素
if c.qcount > 0 {
// Receive directly from queue
qp := chanbuf(c, c.recvx)
// 将recvx上的 数据 写入到ep中
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清空recvx上的数据
typedmemclr(c.elemtype, qp)
// index++
c.recvx++
// 环形数组操作
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// 元素数量--
c.qcount--
unlock(&c.lock)
// 返回true,true,说明channel中有元素,并且channel没有关闭
return true, true
}
if !block {
unlock(&c.lock)
// 通道不想阻塞而返回
return false, false
}
gp := getg()
mysg := acquireSudog()
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// 包装sudog进入读队列(recvq)
c.recvq.enqueue(mysg)
gp.parkingOnChan.Store(true)
// 挂起当前gorotine,等待goready唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 清空一些状态
gp.waiting = nil
gp.activeStackChans = false
success := mysg.success
gp.param = nil
mysg.c = nil
// 回收sudog
releaseSudog(mysg)
return true, success
}
往channel读取数据会出现三种情况
case1:channel中有写等待goroutine
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// c:对应的hchan指针
// ep:被发送到channel的变量的地址
// ....
// 加锁
lock(&c.lock)
// 如果通道没有关闭,并且写队列里面有 等待的goroutine
if sg := c.sendq.dequeue(); sg != nil {
// Found a waiting sender. If buffer is size 0, receive value
// directly from sender. Otherwise, receive from head of queue
// and add sender's value to the tail of the queue (both map to
// the same buffer slot because the queue is full).
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 ep != nil {
// channel无容量,将sudog 对应的数据写给ep
recvDirect(c.elemtype, sg, ep)
}
} else {
// 到这一步,channel 环形数组一定是满的(因为sendq里面有等待者)
// recvx对应的位置的地址
qp := chanbuf(c, c.recvx)
// 将qp上的数据(环形缓冲recvx 指向的数据) 写入ep
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 并且sudug上的数据写入 qp
typedmemmove(c.elemtype, qp, sg.elem)
// 读index++
c.recvx++
// 环形数组处理
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// 同步sendx 和 recvx
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
// 重置这个sudog状态
sg.elem = nil
gp := sg.g
// 释放锁
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 唤醒对应的写等待gorotuine
goready(gp, skip+1)
}
- 先拿锁
- 从sendq(写等待队列)里面弹出队列头部的sudog,进入recv流程
- 如果channel无缓冲区,直接读取sudog里面的数据,并唤醒sudog对应goroutine
- 如果channel有缓冲区,读取环形缓冲区头部元素,并将sudog中的元素写入到缓冲区,唤醒sudog对应goroutine
- 释放锁
case2:channel中没有写等待goroutine,并且环形缓冲数组里面有剩余元素
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// c:对应的hchan指针
// ep:被发送到channel的变量的地址
// .....
// 加锁
lock(&c.lock)
// channel中有元素
if c.qcount > 0 {
// 取recvx对应地址上的元素
qp := chanbuf(c, c.recvx)
// 将这个元素 写入到ep中
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清空recvx上的数据
typedmemclr(c.elemtype, qp)
// index++
c.recvx++
// 环形数组操作
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// 元素数量--
c.qcount--
unlock(&c.lock)
// 返回true,true,说明channel中有元素,并且channel没有关闭
return true, true
}
// .....
}
- 先拿锁
- 读取recvx指向的数据
- 读取recvx指向的数据
- 释放锁
case3:channel中没有写等待goroutine,并且环形缓冲数组里面无剩余元素
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// c:对应的hchan指针
// ep:被发送到channel的变量的地址
// .....
// 加锁
lock(&c.lock)
// 获取当前goroutine
gp := getg()
// 获取一个sudog
mysg := acquireSudog()
// 绑定接收指针
mysg.elem = ep
gp.waiting = mysg
// 绑定goroutine
mysg.g = gp
// 绑定channel
mysg.c = c
// 包装sudog进入读队列(recvq)
c.recvq.enqueue(mysg)
// 挂起当前gorotine,等待goready唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// 清空channel状态
gp.waiting = nil
gp.activeStackChans = false
success := mysg.success
gp.param = nil
mysg.c = nil
// 回收sudog
releaseSudog(mysg)
return true, success
}
- 锁保护步骤同样有的
- 获取一个sudog结构,绑定对应的channel,goroutine,还有ep指针
- 将sudog放入channel的读等待队列(recvq)
- runtime.gopark(挂起当前goroutine,可以看作是解绑当前g和m,然后开启下一轮调度)
特殊case
第一种特殊情况:读取的channel为nil
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// 判断channel是否为nil
if c == nil {
// 如果是非阻塞类型,直接返回
if !block {
// 两个false,通道不想阻塞而返回
return false, false
}
// 是阻塞类型(永久性挂起)
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
// .....
}
- 当channel为nil的时候,对channel进行读操作,会导致当前goroutine永久性挂起,如果当前goroutine是main goroutine的话,还会导致整个程序退出
第二种特殊情况:channel已经关闭,并且没有buf里面没有元素
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// ......
// 如果channel已经关闭
if c.closed != 0 {
// 通道已经关闭,返回两个false,通道不想阻塞而返回
if c.qcount == 0 {
unlock(&c.lock)
// 清空ep
if ep != nil {
typedmemclr(c.elemtype, ep)
}
// 返回false,false,说明channel关闭,并且得到零值
return true, false
}
// ......
}
}
- channel已经关闭,并且没有剩余元素,还想读取channel会得到对应类型的零值
channel关闭
管道的关闭很简单,操作如下
ch := make(chan int)
close(ch)
底层其实就是调用了runtime.closechan函数,源码如下:
func closechan(c *hchan) {
// chan为nil,想要执行关闭操作,直接panic
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
// 通道已经关闭,再次执行关闭的话,直接panic
panic(plainError("close of closed channel"))
}
// 通道置为1,表示关闭
c.closed = 1
// 通过一个gList来记录channel中所有goroutine等待者
var glist gList
// 将所有recvq的等待者加入到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 = unsafe.Pointer(sg)
sg.success = false
glist.push(gp)
}
// 将所有sendq的等待者加入到glist中
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
glist.push(gp)
}
unlock(&c.lock)
// 依次唤醒glist中所有等待者
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
这里我们直接按照源码来分析流程
- 如果对一个nil的channel执行close操作,会发生panic
- 加锁
- 如果重复关闭channel,也会panic
- 关闭channel(c.closed置为1)
- 将sendq和recvq里面所有等待者加入到glist中
- 唤醒glist中所有等待者(唤醒sudog对应的goroutine)