1.channel简介
channel用于Goroutine之间的通信,它提供了一种在不同协程之间传递数据的机制。channel是一种类型安全、阻塞的、先进先出(FIFO)的数据结构,确保发送的数据按照发送的顺序接收。
Go语言提供通过通信来共享内存,而不是通过共享内存来通信.(个人理解就是我跟你用的是同样的东西,但是这个东西你有一份,我也有一份,也可以理解是我把要通信的东西拷贝了一份)
2.核心数据结构
go version go1.19.3 linux/amd64
2.1 hchan
channel分为无缓冲和有缓冲两种,有缓冲的channel使用ring buffer(环形缓冲区)来缓存写入的数据,本质是循环数组(为啥用循环数组?普通数组容量固定、更适合指定的空间,且弹出元素时,元素需要全部前移)
type hchan struct {
qcount uint // total data in the queue
dataqsiz uint // size of the circular queue
buf unsafe.Pointer // points to an array of dataqsiz elements
elemsize uint16
closed uint32
elemtype *_type // element type
sendx uint // send index
recvx uint // receive index
recvq waitq // list of recv waiters
sendq waitq // list of send waiters
lock mutex
}
- qcount:循环数组中元素的数量
- dataqsiz:循环数组中的大小,容量
- buf:指向底层环形数组的指针(环形缓冲区)
- elemsize:元素的大小
- closed:是否关闭的标志:0:未关闭,1:已关闭
- elemtype:channel中的元素类型
- sendx:下一次写的位置(当下标超过数组容量最后会回到第一个位置,所以需要有两个字段记录当前写的下标位置)
- recvx:写一次读的位置(当下标超过数组容量最后会回到第一个位置,所以需要有两个字段记录当前写的下标位置)
- recvq:读等待协程队列
- sendq:写等待协程队列
- lock:互斥锁,保证读写channel时并发安全问题
2.2 sendq和recvq
sendq和recvq分别表示阻塞在当前channel上的发送者goroutine和接收者goroutine。其实现为双链表,双链表对应的结构时waitq,节点是sudog。结构如下:
type waitq struct {
first *sudog
last *sudog
}
双链表的节点是sudog,sudog是Goroutine抽象出来的结构体,一个sudog代表一个在等待队列中的g。sudog主要记录了哪个协程正在等待;等待发送/接收的数据在哪里;等待哪个channel;因为Goroutine与同步对象关系是多对多的。
// src/runtime/chan.go
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
// 等待的是哪个channel
c *hchan // channel
}
3. channel的创建
创建channel实际上就是调用runtime.makechan()函数在内存中实例化出一个hchan的结构体,并返回一个指向该结构体的指针,所以channel是引用类型。
3.1 makechan创建channel整体流程
创建channel时主要分为两大块:边界检查和分配内存,分配内存流程如下:
- 如果是无缓冲channel,直接给hchan结构体分配内存并返回指针
- 如果是有缓冲的channel,但是元素不包含指针类型,则一次性为hchan结构体和底层循环数组分配连续内存并返回指针(需要连续内存)
- 如果是有缓冲的channel,并且元素包含指针类型,则分别分配hchan结构体内存和底层循环数组的内存并返回指针(可以利用内存碎片)
3.2 makechan源码
func makechan(t *chantype, size int) *hchan {
// 获取元素
elem := t.elem
// 元素的大小必须小于64k
if elem.size >= 1<<16 {
throw("makechan: invalid channel element type")
}
// 检查是否对齐,设计操作系统的知识
if hchanSize%maxAlign != 0 || elem.align > maxAlign {
throw("makechan: bad alignment")
}
// 计算buf所需要的内存大小
mem, overflow := math.MulUintptr(elem.size, uintptr(size))
if overflow || mem > maxAlloc-hchanSize || size < 0 {
panic(plainError("makechan: size out of range"))
}
// 创建hchan指针
var c *hchan
switch {
case mem == 0:
// 队列buf或者元素大小为0(struct{}元素大小为0):只分配hchan的内存
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结构体和底层循环数组的内存
c = new(hchan)
c.buf = mallocgc(mem, elem, true)
}
// 设置hchan中的elemsuze、elemtype、dataqsiz、lock等属性
c.elemsize = uint16(elem.size)
c.elemtype = elem
c.dataqsiz = uint(size)
lockInit(&c.lock, lockRankHchan)
// 如果启用了debugChan,进行调试
if debugChan {
print("makechan: chan=", c, "; elemsize=", elem.size, "; dataqsiz=", size, "\n")
}
// 返回hchan指针
return c
}
4.写流程
当我们向一个channel发送数据时,其实底层就是调用了runtime.chansend1()函数
chansend1()函数如下:
func chansend1(c *hchan, elem unsafe.Pointer) {
chansend(c, elem, true, getcallerpc())
}
其实主要逻辑还是在chansend()这个函数,我们平常直接使用 ch <- 1,其实就是使用了chansend的阻塞模式。还有非阻塞模式也就是搭配上select case使用channel。下面先介绍阻塞模式。
4.1chansend() -- block
4.1.1 总流程
4.1.2 chansend()源码
chansend接收到的4个参数:
- c:是一个指向hchan类型的指针,表示要接受数据的通道
- ep:是一个unsafe.Pointer类型的指针,接收要发送的数据的地址
- block:表示接收操作的模式。true为阻塞模式,即发送操作将会阻塞,直到有接收者接受元素;false表示非阻塞模式,及发送操作不会阻塞,如果通道已满,发送操作会立即返回(非阻塞模式后面介绍)
- callerpc:发送操作的调用者的程序计数器值
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
// 如果channel为nil,也就是未初始化,此时写数据直接死锁并且throw
if c == nil {
if !block {
return false
}
gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)
throw("unreachable")
}
...
lock(&c.lock)
// 如果channel已经关闭,此时写数据会panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))
}
// 如果阻塞读队列不为空,也就是有等待的接收者
if sg := c.recvq.dequeue(); sg != nil {
// 直接copy一份ep发送给接收者,不用放入buf
send(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true
}
// 如果有缓冲,并且没有满
if c.qcount < c.dataqsiz {
// Space is available in the channel buffer. Enqueue the element to send.
// qp指向循环数组中未使用的位置
qp := chanbuf(c, c.sendx)
if raceenabled {
racenotify(c, c.sendx, nil)
}
// 将发送的数据copy一份写入到qp指向的环形数组中的位置
typedmemmove(c.elemtype, qp, ep)
// 更新下一次写入环形数组的元素index
c.sendx++
// 当最后一个元素已经使用,此时环形队列将再次从0开始循环
if c.sendx == c.dataqsiz {
c.sendx = 0
}
// 队列中的元素数量+1
c.qcount++
unlock(&c.lock)
return true
}
...
// Block on the channel. Some receiver will complete our operation for us.
// 获取当前的Goroutine,用于绑定sudog
gp := getg()
// 获取一个sudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 保存要发送的数据的地址
mysg.elem = ep
mysg.waitlink = nil
// 保存当前的Goroutine
mysg.g = gp
// 没有使用select case
mysg.isSelect = false
// 保存当前的channel
mysg.c = c
gp.waiting = mysg
gp.param = nil
// 把sudog放进写阻塞队列
c.sendq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
// 调用gopark函数将当前Goroutine挂起,状态为_Gwaiting,等待读协程唤醒或者解锁时唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2)
// Ensure the value being sent is kept alive until the
// receiver copies it out. The sudog has a pointer to the
// stack object, but sudogs aren't considered as roots of the
// stack tracer.
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
// 清除sudog
releaseSudog(mysg)
// 检查是否channel已经关闭,关闭就panic
if closed {
if c.closed == 0 {
throw("chansend: spurious wakeup")
}
panic(plainError("send on closed channel"))
}
return true
}
send()函数如下:
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
if raceenabled {
if c.dataqsiz == 0 {
racesync(c, sg)
} else {
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
c.recvx++
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
}
if sg.elem != nil {
// 有阻塞的读协程,直接copy一份要发送的数据ep到阻塞的读协程
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()
}
// 唤醒阻塞的读协程
goready(gp, skip+1)
}
// 把sg的数据直接copy一份到src
func sendDirect(t *_type, sg *sudog, src unsafe.Pointer) {
dst := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
memmove(dst, src, t.size)
}
5.读流程
当我们用一个channel接收数据,其实底层就是调用chanrecv1()
chanrecv1()如下:
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
核心逻辑在chanrecv()这个函数,下面还是以阻塞模式下介绍。
5.1 chanrecv() - block
5.1.1 总流程
5.1.2 chanrecv()源码
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
if debugChan {
print("chanrecv: chan=", c, "\n")
}
// 如果channel未初始化,直接死锁并且throw
if c == nil {
if !block {
return
}
gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)
throw("unreachable")
}
...
lock(&c.lock)
// 如果channel已经关闭,直接返回
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
}
} else {
// 如果有阻塞写协程
if sg := c.sendq.dequeue(); sg != nil {
// 接着判断buf有无元素,
// 有就从buf取出元素copy一份放到ep,然后把阻塞写协程的数据写到buf
// 没有就直接从阻塞写协程copy一份数据到ep
recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
return true, true
}
}
// 如果buf不为空
if c.qcount > 0 {
// 获取要读取的数据在buf中的位置
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
}
// 把读取出来的数据copy一份到ep
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// 清除刚才从buf读取的数据
typedmemclr(c.elemtype, qp)
// 更新buf下一次要读取数据的index
c.recvx++
// 如果到了buf的最后一个,重置为0
if c.recvx == c.dataqsiz {
c.recvx = 0
}
// buf元素-1
c.qcount--
unlock(&c.lock)
return true, true
}
...
// 获取当前的Goroutine,用于绑定sudog
gp := getg()
// 获取一个sudog
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
// 保存读取数据的地址
mysg.elem = ep
mysg.waitlink = nil
gp.waiting = mysg
// 保存当前的Goroutine
mysg.g = gp
mysg.isSelect = false
// 保存当前的channel
mysg.c = c
gp.param = nil
// 把sudog放入阻塞读队列
c.recvq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
// 调用gopark阻塞Goroutine,把状态切换成_Gwaiting,等待被唤醒
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// someone woke us up
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)
return true, success
}
recv()如下:
func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
// 如果buf没有元素
if c.dataqsiz == 0 {
if raceenabled {
racesync(c, sg)
}
// copy一份阻塞写协程的数据到ep
if ep != nil {
// copy data from sender
recvDirect(c.elemtype, sg, ep)
}
} else {
// 如果buf有元素
// 先获取要读buf的哪一条数据
qp := chanbuf(c, c.recvx)
if raceenabled {
racenotify(c, c.recvx, nil)
racenotify(c, c.recvx, sg)
}
// copy data from queue to receiver
// 然后把从buf中获取到的数据copy一份到ep
if ep != nil {
typedmemmove(c.elemtype, ep, qp)
}
// copy data from sender to queue
// 然后把阻塞写协程的数据写的buf
typedmemmove(c.elemtype, qp, sg.elem)
// 更新buf下一次读取数据的index
c.recvx++
// 超过buf的最后一条数据index,重置c.recvx为0
if c.recvx == c.dataqsiz {
c.recvx = 0
}
c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
}
sg.elem = nil
gp := sg.g
unlockf()
gp.param = unsafe.Pointer(sg)
sg.success = true
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
// 唤醒阻塞的写协程
goready(gp, skip+1)
}
// 把sg的数据copy一份到dst
func recvDirect(t *_type, sg *sudog, dst unsafe.Pointer) {
src := sg.elem
typeBitsBulkBarrier(t, uintptr(dst), uintptr(src), t.size)
memmove(dst, src, t.size) // 汇编实现,有兴趣自己看
}
// 把src的数据copy一份到dst
func typedmemmove(typ *_type, dst, src unsafe.Pointer) {
if dst == src {
return
}
if writeBarrier.needed && typ.ptrdata != 0 {
bulkBarrierPreWrite(uintptr(dst), uintptr(src), typ.ptrdata)
}
memmove(dst, src, typ.size)
if writeBarrier.cgo {
cgoCheckMemmove(typ, dst, src, 0, typ.size)
}
}
6.阻塞和非阻塞模式
上面的源码均是以阻塞模式为主线进行讲述,忽略了非阻塞模式的有关处理逻辑
6.1 非阻塞模式逻辑上的区别
非阻塞模式下,读写channel方法通过一个bool型的响应参数,用以标识是否读取、写入成功。
- 所有需要使得当前Goroutine被挂起的操作,在非阻塞模式都会直接返回false
- 所有使得当前Goroutine会进入死锁的操作,在非阻塞模式下都会直接返回false
- 所有能立即完成读取、写入操作的条件下,非阻塞模式下会返回true
6.2 何时进入阻塞模式
默认情况下,读写channel都是阻塞模式,只有在select语句组成的多路复用分支中,channel才会变成非阻塞模式:
ch := make(chan int)
select {
case <- ch:
default:
}
6.3 底层实现
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
return chansend(c, elem, false, getcallerpc())
}
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected, received bool) {
return chanrecv(c, elem, false)
}
同样是调用chanrecv和chansend函数,但是,此时第三个参数传入的block是false,这就导致后续流程会进入非阻塞模式
7. 两种读channel的协议
读取channel时,可以根据第二个bool型的返回值用以判断当前channel是否已经处于关闭状态:
ch := make(chan int,2)
get1 := <- ch
get2, ok := <- ch
底层调用如下:
func chanrecv1(c *hchan, elem unsafe.Pointer) {
chanrecv(c, elem, true)
}
//go:nosplit
func chanrecv2(c *hchan, elem unsafe.Pointer) (received bool) {
_, received = chanrecv(c, elem, true)
return
}
8. close
close一个channel,底层实际上调用了runtime.closechan()函数
8.1 总流程
8.2 closechan源码
这里之所以用了两个for循环,是因为不可能阻塞的读协程队列和写协程队列都同时存在数据。
可以这样假设,
1.假设阻塞的写协程队列有数据,然后有读协程向channel读取数据,此时会进入recv()函数,buf没有数据就直接copy阻塞的写协程的数据到ep,有数据就直接把buf的数据copy一份到ep,然后再把阻塞写协程的数据写入buf,最后唤醒阻塞的写协程,读协程压根就没机会阻塞。
2.假设阻塞的读协程队列有数据,然后有写协程给channel发送数据,此时会进入send()函数,同理,一样没机会
还需要注意的是,close channel之前,如果有阻塞的写协程会panic,看4.1.2源码最后
func closechan(c *hchan) {
// 关闭一个未初始化的channel,直接panic
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
// 关闭一个已经关闭的channel,直接panic
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
if raceenabled {
callerpc := getcallerpc()
racewritepc(c.raceaddr(), callerpc, abi.FuncPCABIInternal(closechan))
racerelease(c.raceaddr())
}
// 设置channel为1,代表关闭,0表示未关闭
c.closed = 1
var glist gList
// release all readers
// 获取所有阻塞的读协程
for {
sg := c.recvq.dequeue()
if sg == nil {
break
}
if sg.elem != nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
// release all writers (they will panic)
// 获取所有阻塞的写协程
for {
sg := c.sendq.dequeue()
if sg == nil {
break
}
sg.elem = nil
if sg.releasetime != 0 {
sg.releasetime = cputicks()
}
gp := sg.g
gp.param = unsafe.Pointer(sg)
sg.success = false
if raceenabled {
raceacquireg(gp, c.raceaddr())
}
glist.push(gp)
}
unlock(&c.lock)
// Ready all Gs now that we've dropped the channel lock.
// 唤醒阻塞的协程
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
9.gopark&goready
gopark就是阻塞当前的Goroutine,源码如下:
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
if reason != waitReasonSleep {
checkTimeouts() // timeouts may expire while two goroutines keep the scheduler busy
}
// 给m加锁
mp := acquirem()
gp := mp.curg
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
// 保存当前Goroutine的运行信息已经waiting reason
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
mp.waittraceev = traceEv
mp.waittraceskip = traceskip
// 给m解锁
releasem(mp)
// can't do anything that might move the G between Ms here.
mcall(park_m)
}
func acquirem() *m {
_g_ := getg()
_g_.m.locks++
return _g_.m
}
接着看park_m():
func park_m(gp *g) {
_g_ := getg()
if trace.enabled {
traceGoPark(_g_.m.waittraceev, _g_.m.waittraceskip)
}
// 变更Goroutine状态为_Gwaiting
casgstatus(gp, _Grunning, _Gwaiting)
// 把当前Goroutine和M解绑
dropg()
if fn := _g_.m.waitunlockf; fn != nil {
ok := fn(gp, _g_.m.waitlock)
_g_.m.waitunlockf = nil
_g_.m.waitlock = nil
if !ok {
if trace.enabled {
traceGoUnpark(gp, 2)
}
casgstatus(gp, _Gwaiting, _Grunnable)
execute(gp, true) // Schedule it back, never returns.
}
}
// 开启新一轮调度
schedule()
}
goready源码如下:
func goready(gp *g, traceskip int) {
systemstack(func() {
ready(gp, traceskip, true)
})
}
接着看ready如下:
func ready(gp *g, traceskip int, next bool) {
if trace.enabled {
traceGoUnpark(gp, traceskip)
}
status := readgstatus(gp)
// Mark runnable.
_g_ := getg()
// 给m加锁
mp := acquirem() // disable preemption because it can be holding p in a local var
if status&^_Gscan != _Gwaiting {
dumpgstatus(gp)
throw("bad g->status in ready")
}
// status is Gwaiting or Gscanwaiting, make Grunnable and put on runq
// 修改Goroutine的状态为_Grunnable
casgstatus(gp, _Gwaiting, _Grunnable)
// 把Goroutine放入本地队列(有概率被放入runnext)
runqput(_g_.m.p.ptr(), gp, next)
// 尝试通过cas唤醒p,让它去找空闲的M组合,然后寻找G调度
wakep()
// 给m解锁
releasem(mp)
}
在channel实现中,协程阻塞和被唤醒,其实就是在阻塞Goroutine之前,把Goroutine的信息保存好,封装到sudog,然后把sudog放入阻塞队列,然后就更改Goroutine状态为_Gwaiting,然后解除Goroutine和M的关系,直接开启下一轮调度,不需要管这个被抛弃的Goroutine。等这个Goroutine满足某些条件自然就会被唤醒,简单举个例子,此时有一个无缓冲的channel,然后也没有阻塞的读协程,然后有个协程向此channel发送数据,然后就会被封装成sudog放到阻塞的写协程队列,然后有协程来此channel读取数据,会直接把刚才被阻塞都的那个协程的sudog拿出来,copy一份sudog里面的要写的数据到读数据存放地址ep,然后会调用goready()把这个阻塞的协程状态切换成_Grunnable,然后放入本地队列(有概率先放入runnext),然后等待执行。