golang中的channel到底是什么-6

121 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 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,返回成功 14.png

// 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 、 小结:

15.png

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 陷入休眠状态是合理的,不过如果不存在等待的计时器,运行时会直接报错并退出程序。 注意:系统并不能检测出所有的死锁!