前言:
如果判断golang的channel是否关闭,我想玩go的同学都知道,data, ok := <- chan,当ok不是true的时候,说明是channel关闭了。 那么问题来了,channel关闭了,我们是否可以立马获取到channel被关闭的状态?我想这个问题不少人没有去想吧?
为什么有这样的问题? 来自我的一个bug,我期初认为close了一个channel,消费端的goroutine自然是可以拿到channel的关闭状态。然而事实并不是这样的。 只有当channel无数据,且channel被close了,才会返回ok=false。 所以,只要有堆积,就不会返回关闭状态。导致我的服务花时间来消费堆积,才会退出。
测试channel的关闭状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// xiaorui.cc 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字段就是标明是否退出的标识。
1 2 3 4 5 6 7 8 |
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为退出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// xiaorui.cc //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的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// xiaorui.cc 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的方法,因为看起来太黑科技了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
// xiaorui.cc 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了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// xiaorui.cc ... 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 } } ... |
总结:
这个问题肯定不是channel的问题了,只能说我对channel理解还是不够,还是要继续钻研golang runtime源码。没了,希望这篇文章对大家有用。
有疑问加站长微信联系(非本文作者)