实现原理
-
每个select 在编译期间会被转换成OSELECT节点,每个OSELECT节点都有对应的OCASE信息,
-
不同的写法,编译器会有不同的操作
-
select 不存在任何的 case;
select 只存在一个 case;
select 存在两个 case,其中一个 case 是 default;
select 存在多个 case; -
相关的函数在: cmd/compile/internal/gc/select.go#walkselectcases
-
第一种情况: select 不存在任何的case
-
既:
-
select{}
-
-
编译后:
if ncas == 0 { return []*Node{mkcall("block", nil, nil)} } block函数为: func block() { gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1) // forever } 这是一个死循环的阻塞函数,永远无法被唤醒
-
-
第二种情况: 只有一个case的情况:
-
既:
-
select { case v, ok <-ch: // case ch <- v ... }
-
-
编译后:
-
if ch == nil { block() } v, ok := <-ch // case ch <- v 既最终退化成了 chan 的recv(block为true)的函数 发送也是同理
-
-
-
第三种情况: 非阻塞(既有一个Default)
-
既:
-
select{ case c<-v: defaukt: ... } 或者是 select{ case v:=<-c: defaukt: ... }
-
对于send的时候,会被编译为
-
func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { return chansend(c, elem, false, getcallerpc()) } 内部就是上一篇channel源码中的非阻塞发送实现
-
-
对于接收也是同理的,会被编译为:
-
func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) { selected, _ = chanrecv(c, elem, false) return } 内部依旧为上一篇channel实现原理的非阻塞接收
-
-
-
当存在多个case时,会进入selectgo核心函数
-
-
select相关结构体
-
src/runtime/select.go
-
分为多个阶段
-
准备阶段
-
// 先申请内存 cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0)) order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0)) ncases := nsends + nrecvs scases := cas1[:ncases:ncases] // pollOrder: 用途在于防止channel被饿死 pollorder := order1[:ncases:ncases] // lockOrder: 防止channel 被死锁 lockorder := order1[ncases:][:ncases:ncases] ... casePC := func(casi int) uintptr { if pcs == nil { return 0 } return pcs[casi] } ... // 对cases 通过fastrandn进行随机排序,对于空的channel,则会直接跳过 norder := 0 for i := range scases { cas := &scases[i] if cas.c == nil { cas.elem = nil // allow GC continue } j := fastrandn(uint32(norder + 1)) pollorder[norder] = pollorder[j] pollorder[j] = uint16(i) norder++ } pollorder = pollorder[:norder] lockorder = lockorder[:norder] // 通过hchan的地址,对cases 进行排序,内部是堆排序 for i := range lockorder { ... 排序 } ... // debug // 最后,根据lockOrder,对所有的channel进行加锁处理 sellock(scases, lockorder)
-
-
运行阶段又分为三阶段
-
运行阶段1: 查找是否有可用的数据
-
var casi int var cas *scase var caseSuccess bool var caseReleaseTime int64 = -1 var recvOK bool // 随机遍历所有的case for _, casei := range pollorder { casi = int(casei) cas = &scases[casi] c = cas.c // 是否是send if casi >= nsends { // 判断是否有sudog 阻塞在send中 sg = c.sendq.dequeue() if sg != nil { // 如果有的话,则取出数据,转交给receiver,然后自己再入队 goto recv } if c.qcount > 0 { // goto bufrecv } if c.closed != 0 { // 如果该goroutine被关闭了,则进入清理操作 goto rclose } } else { // 则为recv if raceenabled { racereadpc(c.raceaddr(), casePC(casi), chansendpc) } if c.closed != 0 { goto sclose } sg = c.recvq.dequeue() if sg != nil { goto send } if c.qcount < c.dataqsiz { goto bufsend } } } // 如果有Default,进入这里说明上述的都没有触发: 既没有数据,也没有可以写的 // 则直接解锁所有的channel,然后return if !block { selunlock(scases, lockorder) casi = -1 goto retc }
-
-
运行阶段2: 打包sudog 到所有的chan 队列中,等待被唤醒
-
// 获取当前goroutine gp = getg() if gp.waiting != nil { throw("gp.waiting != nil") } nextp = &gp.waiting // 根据lockOrder的顺序, for _, casei := range lockorder { casi = int(casei) cas = &scases[casi] c = cas.c // 获取sudog,将该sudog 与该G 绑定 sg := acquireSudog() sg.g = gp sg.isSelect = true sg.elem = cas.elem sg.releasetime = 0 if t0 != 0 { sg.releasetime = -1 } sg.c = c // Construct waiting list in lock order. *nextp = sg nextp = &sg.waitlink // 然后将该sudog 丢入到各自的队列中,等待被唤醒 if casi < nsends { c.sendq.enqueue(sg) } else { c.recvq.enqueue(sg) } } gp.param = nil // 阻塞等待唤醒 atomic.Store8(&gp.parkingOnChan, 1) gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1) gp.activeStackChans = false // 被唤醒之后,又会按lockOrder的顺序,对所有的case 加锁 sellock(scases, lockorder) gp.selectDone = 0 sg = (*sudog)(gp.param) gp.param = nil
-
-
运行阶段3: 当g被唤醒之后,获取数据,并且将其他的case中的sudog出队
-
casi = -1 cas = nil caseSuccess = false sglist = gp.waiting // 先清空数据,可以认为是help gc for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink { sg1.isSelect = false sg1.elem = nil sg1.c = nil } gp.waiting = nil for _, casei := range lockorder { k = &scases[casei] if sg == sglist { // 当前sg 是被该case 唤醒的 casi = int(casei) cas = k caseSuccess = sglist.success if sglist.releasetime > 0 { caseReleaseTime = sglist.releasetime } } else { // 否则的话,把该sudog 从队列中出队,因为sudog 此时可以认为已经是dirty了 c = k.c if int(casei) < nsends { c.sendq.dequeueSudoG(sglist) } else { c.recvq.dequeueSudoG(sglist) } } sgnext = sglist.waitlink sglist.waitlink = nil // 将sudog 与g 解绑,内部也会解绑p releaseSudog(sglist) sglist = sgnext } ... debug if casi < nsends { if !caseSuccess { goto sclose } } else { recvOK = caseSuccess } .... debug // 最后再对有的case 解锁 selunlock(scases, lockorder) goto retc
-
-
-
总结
-
select中有2个关键切片
- pollOrder和lockOrder
- pollOrder使得,channel不会被饥饿死,并且,可以防止死锁
-
for select 等待的时候,会先使用pollOrder,可以提前判断是否已经有数据读或者写,有的话则会直接send/recv ,然后return
-
之后通过lockOrder打包sudog ,放入到各自的case的chan的对应的队列中,等待被唤醒
-
被唤醒之后,对于其他的case,会将这个脏的sudog 出队(因为此时可以认为sudog被某个case占据了)
-
内部有许多goto 方法,但是其实掉的还是和chan的类似,send/recv/close
问题
-
lockOrder的sellock 的作用是什么
- selllock的作用: 加锁, 因为chan内部对于数据的操作都是通过锁来操作的
- lockOrder 是为了将所有的case 根据hchan的地址顺序加锁
-
pollOrderer和lockOrderer的作用,以及 pollOrder为什么要随机排序
- pollOrder: 2个目的:
- 快速判断是否有数据已经准备就绪
- 为了防止某个chan被饿死
- lockOrder: 是为了防止死锁:
- 如 chan 互相等待导致死锁
- pollOrder: 2个目的: