持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第31天,点击查看活动详情
3.4.3、buffer中接收
当qcount 大于0 时 ,表示buf 中有数据,此时直接将recvx 中数据拷贝到目标地址,然后recvx +1 返回成功
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
3.4.4、阻塞接收
如果上两个步骤均不满足,也就是说 没有找到发送方并且缓冲区为0, 将会获取一个sudog包裹当前协程,加入到 channel recvq 的队列尾部,然后让出当前协程 ,等待唤醒;当被其他协程唤醒后,将释放当前sudog,返回成功
// no sender available: block on this channel.
gp := getg()
mysg := acquireSudog()
mysg.releasetime = 0
if t0 != 0 {
mysg.releasetime = -1
}
mysg.elem = ep
gp.waiting = mysg
mysg.g = gp
mysg.isSelect = false
mysg.c = c
c.recvq.enqueue(mysg)
atomic.Store8(&gp.parkingOnChan, 1)
gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanReceive, traceEvGoBlockRecv, 2)
// ...
releaseSudog(mysg)
return true, success
3.4.5 、 小结:
3.5 、channel关闭
从源码可以看到,关闭channel 总体分为两步:
- 关闭前的检测:关闭一个未初始化的channel 以及 关闭一个已经关闭的channel 都会导致panic
- 遍历获取channel 上挂载的所有readers 以及 senders ,加入到对应的runnext 队列中
func closechan(c *hchan) {
if c == nil {
panic(plainError("close of nil channel"))
}
lock(&c.lock)
if c.closed != 0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))
}
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
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
glist.push(gp)
}
unlock(&c.lock)
for !glist.empty() {
gp := glist.pop()
gp.schedlink = 0
goready(gp, 3)
}
}
从这里可以引申一个问题:如何优雅的关闭channel ? 以下代码是否可以完成对close 的校验
func IsClosed(ch <-chan T) bool {
select {
case <-ch:
return true
default:
}
return false
}
答案是否定的,select 与return 直接可能存在并发 导致return的时候可能已经被关闭了
在使用Go channel的时候,一个适用的原则是不要从接收端关闭channel,也不要关闭有多个并发发送者的channel。换句话说,如果sender(发送者)只是唯一的sender或者是channel最后一个活跃的sender,那么你应该在sender的goroutine关闭channel,从而通知receiver(s)(接收者们)已经没有值可以读了。维持这条原则将保证永远不会发生向一个已经关闭的channel发送值或者关闭一个已经关闭的channel。 通常会使用以下几种办法close channel
- 暴力法
func SafeClose(ch chan T) (justClosed bool) {
defer func() {
if recover() != nil {
justClosed = false
}
}()
close(ch)
return true
}
- sync.Once
type MyChannel struct {
C chan T
once sync.Once
}
func NewMyChannel() *MyChannel {
return &MyChannel{C: make(chan T)}
}
func (mc *MyChannel) SafeClose() {
mc.once.Do(func(){
close(mc.C)
})
}
- 对于M个receiver,N个sender 这种生产者消费组的模式,可以采用以下方式进行关闭
// 生产者数量
lenSenders := 5
// 消费着数量
lenReaders := 10
sendCloseCh := make(chan struct{})
// 关闭触发信号
stopCh := make(chan struct{})
// 数据channel
dataCh := make(chan struct{})
go func() {
for i := 0; i < lenSenders; i++ {
<-sendCloseCh
}
// 等待全部生产者推出以后 关闭数据通道
fmt.Println(fmt.Sprintf("所有生产者退出完毕"))
close(dataCh)
}()
for i := 0; i < lenSenders; i++ {
go func(index int) {
start:
for {
select {
case <-stopCh:
break start
default:
// 这里执行用户真正的逻辑 需要加defer 保护,防止意外panic 导致无法退出
dataCh <- struct{}{}
}
}
sendCloseCh <- struct{}{}
fmt.Println(fmt.Sprintf("index=%d,退出", index))
}(i)
}
for i := 0; i < lenReaders; i++ {
go func() {
for data := range dataCh {
fmt.Println("消费数据")
_ = data
time.Sleep(time.Second)
}
}()
}
time.Sleep(1 * time.Second)
// 执行close
close(stopCh)
3.6 、死锁的检测
关于死锁检测可以查看 检查死锁 章节;
当运行时存在等待的 Goroutine 并且不存在正在运行的 Goroutine 时,我们会检查处理器中存在的计时器1:如果处理器中存在等待的计时器,那么所有的 Goroutine 陷入休眠状态是合理的,不过如果不存在等待的计时器,运行时会直接报错并退出程序。 注意:系统并不能检测出所有的死锁!