Channel是什么
// 不用通过共享内存来通信,而是要通过通信来共享内存;
Do not communicate by sharing memory; instead, share memory by communicating.
Channel就是Go的通信工具,是Go内置的“MQ”,可支持Go的任何内置类型、以及用户自定义的struct、指针等,原生支持并发
特性
在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据。goroutine 间通过通道就可以通信。
通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。
Channel用法
初始化
- 引用类型
ch := make(chan int)
- 操作符
<- 可用于chan的读写,数据总是从右侧流向左侧
- chan类型(权限控制)
// 可读可写:chan
ch := make(chan int)
// 只读:<- chan
ch := make(<-chan int)
// 只写:chan <-
ch := make(chan<- int)
- 大小设置
// 有缓冲:可设置chan大小
ch := make(chan int, 10)
// 无缓冲:不设置chan大小即为无缓冲
ch := make(chan int)
- 元素类型
// chan的元素类型:除上述int外,ElementType可以是任何类型
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType
读写
- 读
<- ch
- 写
ch <- a
阻塞式与非阻塞式
- 阻塞式
// 读
a := <- ch
for a := range ch {
...
}
<- ch
// 写
ch <- a
- 非阻塞式
// 读
a, ok := <- ch
select {
case <- ch:
...
}
// 写
select {
case ch <- a:
...
}
关闭channel
close(ch)
注意点
- 向已关闭的chan写入数据,将会导致panic
close(ch)
ch <- a // panic: send on closed channel
- 关闭chan时会写入一个空值到ch中
Channel实现原理
数据结构
type hchan strcut {
qcount uint // chan中元素数量
dataqsiz uint // chan大小
buf unsafe.Pointer // buf数组的地址
elemsize uint16 // chan中元素大小
closed uint32 // chan是否关闭
elemtype *_type // 元素类型
sendx uint // 写索引
recvx uint // 读索引
recvq waitq // 接收等待者列表
sendq waitq // 发送等待者列表
lock mutex
}
- qcount:chan当前的元素数量
- dataqsiz:chan的容量,即make(chan elemtype, n)里的第二个参数n
- buf:buf元素数组的地址
- elemsize:chan里所装的元素的大小
- sendx:写索引,即当前写到什么位置了
- recvx:读索引,读到什么位置了
- recvq:读队列,双向链表
- sendq:写队列,双向链表
- lock:chan内部有一把互斥锁,使得同时只能有一个gorotine进行发收,保障了数据的顺序性
chan的buf数组是一个抽象的环形结构,如图:
type sudog struct {
g *g
next *sudog
prev *sudog
elem unsafe.Pointer // data element (may point to stack)
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
}
sudog是chan的读写队列的元素,它封装了收发元素的Gorotine
初始化Chan
func makechan(t *chantype, size int) *hchan {
elem := t.elem
// 参数校验
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
...
// 计算需要分配的内存
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
// 根据元素大小等设置chan的参数
...
// 初始化chan
...
return c
}
创建chan主要分两步,一是参数校验,二是初始化chan结构。
初始化chan结构主要分为三种情况:
switch {
case mem == 0:
// Queue or element size is zero.
c = (*hchan)(mallocgc(hchanSize, nil, true))
// Race detector uses this location for synchronization.
c.buf = c.raceaddr()
case elem.ptrdata == 0:
// Elements do not contain pointers.
// Allocate hchan and buf in one call.
c = (*hchan)(mallocgc(hchanSize+mem, nil, true))
c.buf = add(unsafe.Pointer(c), hchanSize)
default:
// Elements contain pointers.
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)
-
不带buffer的chan或者元素大小为0的chan,只需要分配chan自己的内存空间
-
非指针元素,内存可以是连续的,所以分配各元素和chan本身的内存
-
指针元素,内存不是连续的,先new一个chan,再单独分配元素buffer的数组
发送数据
发送主要分为以下几个步骤:
- chan为nil的情况处理
- chan已关闭,直接panic
- 消费队列不为空(消费方有需求),直接给他
- 消费队列为空(无消费需求),chan没满直接塞进去,chan满了,判断是否阻塞发送
发送给消费者
// 调用send函数
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if sg.elem != nil {
// 将发送的值直接拷贝到接收值的内存地址
sendDirect(c.elemtype, sg, ep)
sg.elem = nil
}
// 获取等待接收数据的goroutine
gp := sg.g
// 解锁
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
// 唤醒等待接收数据的goroutine
goready(gp, skip+1)
}
主要步骤为:
- 拷贝发送元素
- 获取等待接收数据G
- 唤醒等待接收数据的G
发送到chan
// 获取当前发送索引的地址
qp := chanbuf(c, c.sendx)
// 将元素写入到chan队尾
typedmemmove(c.elemtype, qp, ep)
// sendx自增
c.sendx++
// 若发送索引超过队列容量,将发送索引置为0(环形队列)
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// chan元素数量+1
c.qcount++
主要步骤为:
- 获取当前发送索引的地址
- 将元素拷贝到chan队尾
- sendx+1,元素数量+1
阻塞发送
// 获取当前goroutine
gp := getg()
// 创建一个sudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 给sudog赋值,发送元素->elem,gp->g,chan->c
mysg.elem = ep
mysg.waitlink = nil
mysg.g = gp
mysg.isSelect = false
mysg.c = c
// 将sudog和gorotuine绑定
gp.waiting = mysg
gp.param = nil
// 进入写入队列
c.sendq.enqueue(mysg)
// 挂起goroutine,进入等待状态
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
KeepAlive(ep)
if mysg != gp.waiting {
throw("G waiting list is corrupted")
}
// 与goroutine、chan解绑,并释放sudog
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"))
}
主要步骤为:
- 获取当前G,创建一个sudog
- 将G和SG以及chan绑定,sudog进入写阻塞队列
- 挂起goroutine
- 被唤醒后解绑,释放sudog
接收数据
主要分为以下几个步骤:
- chan为nil的情况处理
- chan已关闭,且没有元素,返回false
- 发送队列不为空,直接接收
- 发送队列为空,chan中有元素则直接读取,chan无元素则阻塞
从发送者接收
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// chan无缓冲
if c.dataqsiz == 0 {
if ep != nil {
// 直接将发送队列中的数据拷贝到接收值
recvDirect(c.elemtype, sg, ep)
}
} else {
// 有缓冲,而且chan满了
// 取recv索引地址
qp := chanbuf(c, c.recvx)
if ep != nil {
// 拷贝recv索引处的值给接收值
typedmemmove(c.elemtype, ep, qp)
}
// 将发送者的值拷贝到recv索引处
typedmemmove(c.elemtype, qp, sg.elem)
// recv索引+1
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// send索引=recv索引,标记队尾
c.sendx = c.recvx
}
// 修改发送者sg状态
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
// 唤醒发送者的goroutine
goready(gp, skip+1)
}
主要步骤为:
- 无缓冲的chan直接赋值给接收者
- 有缓冲的,拷贝chan中队首的值给接收者,再将发送者的值拷贝到队首,recv索引+1,队首变队尾
- 唤醒等待发送数据的G
从chan接收
// 取recvx地址
qp := chanbuf(c, c.recvx)
if ep != nil {
// 拷贝recvx处的值到接收值
typedmemmove(c.elemtype, ep, qp)
}
// 清除chan上recvx的值
typedmemclr(c.elemtype, qp)
// recv索引+1
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// chan元素数量-1
c.qcount--
主要步骤为:
- 拷贝recvx处的值到接收值
- 清除chan上recvx的值
- recv索引+1,chan元素数量-1
阻塞接收
// 获取当前goroutine
gp := getg()
// 创建sudog
mysg := acquireSudog()
mysg.releasetime = 0
// 绑定sg和g
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
gp.param = nil
// sg进入chan的接收队列
c.recvq.enqueue(mysg)
// 挂起g,阻塞接收
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)
}
// 标记sg状态
success := mysg.success
gp.param = nil
mysg.c = nil
// 释放sg
releaseSudog(mysg)
主要步骤为:
- 获取当前g,创建sudog,并将sg和g绑定
- sg进入chan的接收队列
- 挂起g,阻塞接收
- 标记sg状态解绑g,释放sg
关闭Chan
func closechan(c *hchan) {
// 判断chan是否为nil
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
// 已关闭的chan,触发panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
// 标记close
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
}
// 将阻塞接受者的goroutine加入到glist
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
glist.push(gp)
}
// 释放所有发送者
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
// 将阻塞发送者的goroutine加入到glist
sg.elem = nil
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
glist.push(gp)
}
unlock(&c.lock)
// 唤醒所有阻塞的发送者和接受者
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
主要分为以下几个步骤:
- chan为nil,则panic
- chan已关闭,触发panic
- 标记chan的close状态
- 释放接受者和发送者,并将阻塞的g加入到glist
- 统一唤醒glist中的g,使得g释放