channel结构体
-
src/runtime/chan.go
-
内部存储是以循环队列的形式存储额
-
type hchan struct { qcount uint // 代表着这队列中总共有多少元素 dataqsiz uint // 创建时的容量设置,既make时候的大小, buf unsafe.Pointer // 存储的数据 elemsize uint16 // chan数据对象的自身大小,既make的时候的参数类型的大小 closed uint32 // 标识关闭状态 elemtype *_type // 代表chan中的元素类型 sendx uint // 待发送的索引,既循环队列的队尾指针tail recvx uint // 待读取的索引,既循环队列的队头head recvq waitq // 接收方 的等待队列 sendq waitq // 发送方 的等待队列 lock mutex }
channel的创建
-
相关常量定义
-
const ( maxAlign = 8 hchanSize = unsafe.Sizeof(hchan{}) + uintptr(-int(unsafe.Sizeof(hchan{}))&(maxAlign-1)) debugChan = false )- maxAlign: 这个与操作系统有关,cpu有三级缓存,L1,L2,L3,L3cpu之间共享,数据以cacheline的形式存储在寄存器中,读取的时候,会通过总线广播其他cpu,标识cache line 无效, 如果cache line 中存储的不是同一种数据,会使得触发伪共享问题,而maxAlign就是为了使得,该数据刚好占满整个cache line (空间换时间的做法)
- hchansize: 与Java的hashMap类似,都是为了使得cap 为2的n次方
-
-
chan分为: 无缓冲chan和有缓存chan
- 无缓冲既: make(chan int) : 这时候可以认为只是用于 通知
- 有缓冲: make(chan int,10) : 可以用于数据传输
-
源码分析
-
func makechan(t *chantype, size int) *hchan { // 获取元素 elem := t.elem // 获取make的元素的所占内存大小,可以发现对象的大小是有限定 if elem.size >= 1<<16 { throw("makechan: invalid channel element type") } // 不同的操作系统cacheline的大小是不同的,但是都是2的n次方 if hchanSize%maxAlign != 0 || elem.align > maxAlign { throw("makechan: bad alignment") } // 当乘积过大,超出内存时候,panic mem, overflow := math.MulUintptr(elem.size, uintptr(size)) if overflow || mem > maxAlloc-hchanSize || size < 0 { panic(plainError("makechan: size out of range")) } // 注意: 当如果创建的对象不是指针的话,gc不会对该部分进行检测回收 var c *hchan switch { case mem == 0: // 创建的是,无缓冲通道 // 申请内存空间,可以发现,当为无缓冲的通道的时候,创建的是空值 c = (*hchan)(mallocgc(hchanSize, nil, true)) c.buf = c.raceaddr() case elem.ptrdata == 0: // 如果 chan的数据非指针对象,则一次创建全部: c = (*hchan)(mallocgc(hchanSize+mem, nil, true)) c.buf = add(unsafe.Pointer(c), hchanSize) default: // 当包含指针时,2次创建 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 }
-
chan的发送
-
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { // 如果为空,且阻塞,直接挂起该goroutine跑出异常 if c == nil { if !block { return false } gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2) throw("unreachable") } .... 省略一些debug // 写数据的时候是必须要加锁的,但是加锁的性能损耗大,所以加锁前会先提前判断是否可以退出 // 当非阻塞写 + 未关闭+ 如果数据满了 if !block && c.closed == 0 && full(c) { return false } var t0 int64 if blockprofilerate > 0 { t0 = cputicks() } // 开始加锁 lock(&c.lock) // 安全性校验 if c.closed != 0 { unlock(&c.lock) panic(plainError("send on closed channel")) } // 如果当前等待队列中有在等待的,则会直接将该数据dropoff给这个receiver,不将数据保存到buf中 if sg := c.recvq.dequeue(); sg != nil { send(c, sg, ep, func() { unlock(&c.lock) }, 3) return true } // 如果还有空间剩余 if c.qcount < c.dataqsiz { qp := chanbuf(c, c.sendx) if raceenabled { racenotify(c, c.sendx, nil) } // 将该实例数据拷贝buf中 typedmemmove(c.elemtype, qp, ep) // 然后更新索引,环形数组的话,下次还会从0开始 c.sendx++ if c.sendx == c.dataqsiz { c.sendx = 0 } // 更新拥有的数据数量 c.qcount++ unlock(&c.lock) return true } // 进入这,说明没有空间剩余了,如果非阻塞,则直接return(既select-default的做法) if !block { unlock(&c.lock) return false } // 开始挂起,获取当前的goroutine gp := getg() // 获取sudog ,打包当前对象,并且与当前的g所绑定 mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } mysg.elem = ep mysg.waitlink = nil mysg.g = gp mysg.isSelect = false mysg.c = c gp.waiting = mysg gp.param = nil // 更新sudog到发送的等待队列中 c.sendq.enqueue(mysg) // 调用gopark 进行阻塞 atomic.Store8(&gp.parkingOnChan, 1) gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2) KeepAlive(ep) // 被唤醒之后,先进行安全性校验,如果当前的sudog与设置的不符合,则认为是数据被破坏了 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 } -
func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { // 如果接收方是有实例数据等待接收 if sg.elem != nil { // 则直接将数据拷贝到receiver的接收数据结构中 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) }
channel的接收
-
// 第一个参数是chan的指针, 第二个参数是 func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) { // 与send的时候同理,对于nil channel直接挂起 // 同样,与send 同理,会有fastFail 校验,来避免加锁 if !block && empty(c) { if atomic.Load(&c.closed) == 0 { return } if empty(c) { if raceenabled { raceacquire(c.raceaddr()) } if ep != nil { typedmemclr(c.elemtype, ep) } return true, false } } var t0 int64 if blockprofilerate > 0 { t0 = cputicks() } // 加锁 lock(&c.lock) // 如果chan已经close了,并且没有任何数据 if c.closed != 0 && c.qcount == 0 { if raceenabled { raceacquire(c.raceaddr()) } // 则直接解锁,然后 unlock(&c.lock) if ep != nil { // 将返回值置为0值 typedmemclr(c.elemtype, ep) } return true, false } // 如果有sender 阻塞,则直接pop出第一个,直接从发送方获取数据 if sg := c.sendq.dequeue(); sg != nil { recv(c, sg, ep, func() { unlock(&c.lock) }, 3) return true, true } // 没有sender在发送数据,同时缓冲中有数据,则直接从缓冲中读取数据 if c.qcount > 0 { // 则通过recvindex 得到数据 qp := chanbuf(c, c.recvx) if raceenabled { racenotify(c, c.recvx, nil) } // 然后将数据内容拷贝到参数中 if ep != nil { typedmemmove(c.elemtype, ep, qp) } typedmemclr(c.elemtype, qp) c.recvx++ if c.recvx == c.dataqsiz { c.recvx = 0 } c.qcount-- unlock(&c.lock) return true, true } // 没数据,且异步,则直接返回 if !block { unlock(&c.lock) return false, false } // 说明什么数据也没有,开始挂起该goroutine gp := getg() // 获取sudog,打包成sudog,将sudog 与当前goroutine绑定 mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } mysg.elem = ep mysg.waitlink = nil gp.waiting = mysg mysg.g = gp mysg.isSelect = false mysg.c = c gp.param = nil // 将该recv的goroutine入队 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) return true, success } -
如果有sender 在等待队列中等待,则直接从sender中拿去数据 func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) { // 如果是无缓冲的chan if c.dataqsiz == 0 { if raceenabled { racesync(c, sg) } if ep != nil { // 从直接从sender的sudog 中提取数据 recvDirect(c.elemtype, sg, ep) } } else { 进入到这里,说明缓冲是满的 1. 从缓冲中拿去数据 2. 更新recvIndex 3. 更新sendIndex qp := chanbuf(c, c.recvx) if raceenabled { racenotify(c, c.recvx, nil) racenotify(c, c.recvx, sg) } // copy data from queue to receiver if ep != nil { typedmemmove(c.elemtype, ep, qp) } // copy data from sender to queue 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 } sg.elem = nil // 可以认为是快速清除引用 gp := sg.g unlockf() gp.param = unsafe.Pointer(sg) // 然后跟新sender 中的sudog 为success sg.success = true if sg.releasetime != 0 { sg.releasetime = cputicks() } // 通知sender ,唤醒sender goready(gp, skip+1) }
总结
-
数据通信不是通过共享内存的形式来通信,而是通过 copy 内存的形式,既发送和接收得到数据都会触发拷贝操作
-
golang的hchan,内部是回环数组实现数据的存储
-
对于nilchannel 会触发挂起
-
无论是send还是recv,当都会有一个快速失败的校验,因为数据写入和读取都是需要加锁的
-
无论是send还是recv,都会先判断是否 recv队列和send队列是否为空,不为空的话
- 对于recv而言,则会先直接从发送队列中取出sudog,然后从环形队列中取出值给recv之后,将发送队列的sudog的值追加到环形队列中
- 对于send而言,则直接从recv队列中拿出sudog,然后把要发送的数据直接转交给这个receiver
- 最后,都会唤醒从队列取出的sudog所绑定的goroutine
问题
-
chan无缓冲和有缓冲的区别
- 无缓冲的话,初始化make的时候,申请内存的时候是一个空值
- 无缓冲的chan,读写必须在不同的routine
-
当chan为无缓冲的时候,send和recv会做什么
- send和recv 必须是 不在同一个routine才行
-
recv的时候什么情况下,参数会为空
- 当 <-c 的时候,参数是空参数
-
closed := !mysg.success 的触发情况有哪些
- 当close的时候,close的时候会将等待队列中所有的succes都设置为false
-
send 什么时候被唤醒,是怎么被唤醒的
- when: 如果buffer满的时候,send的goroutine会被打包成sudog,然后丢入到发送队列中,等待被唤醒
- how: 通过goready 被唤醒,当recv的时候,如果有send在等待,则会将数据从缓冲数据取出来之后,唤醒等待队列中的头节点
-
什么是sudog
-
代表的是一个 g 的等待队列,因为g 会发生复用,一个g所关联的同步对象可能是n个,
-
每个sudog代表的都是g所关联的一个对象
-
-
怎样的才是 非阻塞的发送数据给chan
- 配合select 的发送,既是异步发送
-
chan 非指针对象和指针对象的区别
- 内存分配都是一次性分配的
- 非指针对象,在申请内存的时候,只会申请一次(既chan的固定大小+计算得出的mem大小)
- 指针对象: 会触发2次申请内存,第一次为先申请hchan的内存,然后申请指针的内存
-
recvq和sendq的作用
- recvq用于存储 <-c 这个操作的routine成员,当sendQ和没没有数据的时候,会打包成sudog,然后挂起
- sendq用于存储 c<- 这个操作的goroutine,当buffer满的时候,则会打包成sudog ,然后添加到sendq,然后挂起
-
会发现在hchan中的有几个成员变量似乎是冲突的,如 qcount ,elemsize ,不都是标识长度的吗
本文详细介绍了Go语言中channel的内部结构,包括hchan的字段和其作用,如循环队列、等待队列等。文章还分析了channel的创建过程,以及无缓冲和有缓冲chan的区别。在发送和接收操作中,讨论了快速失败、阻塞、数据拷贝等关键点,并展示了如何处理满缓冲和空缓冲的情况。最后,文章提到了sudog结构,它作为goroutine等待队列的一部分,用于管理goroutine的挂起和唤醒。
2386

被折叠的 条评论
为什么被折叠?



