赞
踩
我们都知道data, ok := <- chan第一个变量表示读出的数据,第二个变量表示是否成功读取了数据,有意思的是,第二个变量并不用于指示管道的关闭的状态。第二个变量常常被误以为关闭状态是因为它确实和管道状态有关,确切的来说,是和管道缓冲区是否有数据有关。
如果判断golang的channel是否关闭,data, ok := <- chan,当ok不是true的时候,说明是channel关闭了。 那么问题来了,channel关闭了,我们是否可以立马获取到channel被关闭的状态?我想这个问题不少人没有去想吧?为什么有这样的问题? 来自我的一个bug,我期初认为close了一个channel,消费端的goroutine自然是可以拿到channel的关闭状态。然而事实并不是这样的。 只有当channel无数据,且channel被close了,才会返回ok=false。 所以,只要有堆积,就不会返回关闭状态。导致我的服务花时间来消费堆积,才会退出。
测试channel的关闭状态
- package main
-
-
-
- import (
-
- "fmt"
-
- )
-
-
-
- func main() {
-
- c := make(chan int, 10)
-
- c <- 1
-
- c <- 2
-
- c <- 3
-
- close(c)
-
- for {
-
- i, ok := <-c
-
- fmt.Println(ok)
-
- if !ok {
-
- fmt.Println("channel closed!")
-
- break
-
- }
-
- fmt.Println(i)
-
- }
-
- }
我们发现已经把channel关闭了,只要有堆积的数据,那么ok就不为false,不为关闭的状态。
go runtime channel源码分析
首先我们来分析下go runtime/chan.go的相关源码,记得先前写过一篇golang channel实现的源码分析,有兴趣的朋友可以翻翻。 这次翻channel源码主要探究下close chan过程及怎么查看channel是否关闭?
下面是channel的hchan主数据结构,closed字段就是标明是否退出的标识。
- 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
-
- ...
-
- }
下面是关闭channel的函数,修改了closed字段为1, 1为退出。
- //go:linkname reflect_chanclose reflect.chanclose
-
- func reflect_chanclose(c *hchan) {
-
- closechan(c)
-
- }
-
-
-
- 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
-
- ...
-
- }
下面是channel的recv消费者方法,也就是 data, ok := <- chan。if c.closed != 0 && c.qcount == 0 只有当 closed为1 并且 堆积为0的时候,才会返回false。 一句话,channel已经closed,并且没有堆积任务,才会返回关闭channel的状态。
- func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
-
- ....
-
- lock(&c.lock)
-
-
-
- if c.closed != 0 && c.qcount == 0 {
-
- if raceenabled {
-
- raceacquire(unsafe.Pointer(c))
-
- }
-
- unlock(&c.lock)
-
- if ep != nil {
-
- typedmemclr(c.elemtype, ep)
-
- }
-
- return true, false
-
- }
-
-
-
- if sg := c.sendq.dequeue(); sg != nil {
-
- recv(c, sg, ep, func() { unlock(&c.lock) }, 3)
-
- return true, true
-
- }
-
-
-
- if c.qcount > 0 {
-
- // Receive directly from queue
-
- qp := chanbuf(c, c.recvx)
-
- c.qcount--
-
- unlock(&c.lock)
-
- return true, true
-
- }
-
- ...
-
- }
channel代码里没有找到一个查询channel关闭的方法。
解决方法
那么如何在channel堆积的情况下,得知channel已经关闭了 ?
第一种方法:
可以直接读取channel结构hchan的closed字段,但问题chan.go没有开放这样的api,所以我们要用reflect这个黑科技了。 (不推荐大家用reflect的方法,因为看起来太黑科技了)
- import (
-
- "unsafe"
-
- "reflect"
-
- )
-
-
-
- func isChanClosed(ch interface{}) bool {
-
- if reflect.TypeOf(ch).Kind() != reflect.Chan {
-
- panic("only channels!")
-
- }
-
- cptr := *(*uintptr)(unsafe.Pointer(
-
- unsafe.Pointer(uintptr(unsafe.Pointer(&ch)) + unsafe.Sizeof(uint(0))),
-
- ))
-
-
-
- // this function will return true if chan.closed > 0
-
- // see hchan on https://github.com/golang/go/blob/master/src/runtime/chan.go
-
- // 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
-
- // **
-
-
-
- cptr += unsafe.Sizeof(uint(0))*2
-
- cptr += unsafe.Sizeof(unsafe.Pointer(uintptr(0)))
-
- cptr += unsafe.Sizeof(uint16(0))
-
- return *(*uint32)(unsafe.Pointer(cptr)) > 0
-
- }
第二种方法:
配合一个context或者一个变量来做。就拿context来说,那么select不仅可以读取数据chan,且同时监听<- context.Done() , 当context.Done()有事件,直接退出就ok了。
- ...
-
- ctx, cancel := context.WithCancel(context.Background())
-
- close(c)
-
- cancel()
-
- exit:
-
- for {
-
- select {
-
- case data, ok := <-c:
-
- fmt.Println(data, ok)
-
-
-
- case <-ctx.Done():
-
- break exit
-
- }
-
- }
-
- ...
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。