golang channel提前close丢失数据?

前言:

       很无聊,闲来无事看golang一些开源代码,发现有些人作为生产者把消息扔到channel就直接顺手给close掉了,也不等消费者消费完。有些惊奇,这不会丢数据么?按照字面上的意思,我既然close了,呢么消费者应该被唤醒退出。 但经过我的测试发了大量 ch <- data 之后,立马close的话,消费者还是照样会消费数据,不会因为close channel丢消息。

该文章后续仍在不断更新中, 请移步到原文地址 http://xiaorui.cc/?p=5007

     那么我们来分析下 channel 为什么不会丢数据 ?  涉及到原理性的话题,不要猜,只能看golang源码了。 golang channel源码里面的hchan结构,close方法及chanrecv方法就可以完美的解释刚才的问题。  channel源码地址, https://golang.org/src/runtime/chan.go,大家可以对照下源码。

Hchan

Hchan是channel的主要数据结构,我们关心的 qcount用来表示消息的个数,closed int32标识用来表示chan的开关,1为关闭.

# xiaorui.cc

type hchan struct {
  qcount   uint           // total data in the queue
  ...
  closed   uint32
  ...

Closechan

Closechan是chan.go里的关闭channel的方法,该方法除了将 c.closed 设置为 1。还需要唤醒 recvq和sendq 队列里面的阻塞 goroutine. 如果你不唤醒的化,因为没有生产者send数据,也就无法通过事件来唤醒goroutine,所以这时候需要把他们都唤醒起来,判断是否退出的逻辑。

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
    ....

Chanrecv

消费者接收事件相关的逻辑,不是简单的判断closed标志位,而且会判断channel消息个数…  就因为不好好看源码,导致我想让消费者退出的时候,用另外一个exit channel来通信,这样每个消费者用select监听两个channel,一个业务数据,一个通知退出…  

玩了几年channel,居然不知道这特性,也是醉了。 这里我给自己解释下,我开发的后端服务基本是那种channel作为队列常住的那样,不会像有些服务动不动就new一个channel,所以…. 

# 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
}

...

通过上面的channel源码,我们知道了channel是安全退出的。下面是我测试的脚本,我兴趣的可以跑跑。

// xiaorui.cc 

package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    ch := make(chan int, 2000)

    var wg sync.WaitGroup

    go func() {
        for i := 0; i < 200; i++ {
            ch <- i
        }
        close(ch)
    }()

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go work(&wg, ch)
    }
    wg.Wait()
    fmt.Println("exit")
}

func work(wg *sync.WaitGroup, ch chan int) {
    defer wg.Done()
    for {
        time.Sleep(100 * time.Millisecond)
        select {
        case data, ok := <- ch:
            fmt.Println(data, ok)
        default:
            fmt.Println("not data")
        }

        item, ok := <-ch
        if !ok {
            break
        }
    }
}

总结:

     多看源码,多读书 .  golang社区里的源码质量大多质量比较高,建议大家多看点源码。 先前看了 golang nsq, boltdb, influxdb的一些源码,感触很多.  当你不知道该学点什么的时候,可以随意在github中闲逛下,总有点项目会让你眼前一亮。


大家觉得文章对你有些作用! 如果想赏钱,可以用微信扫描下面的二维码,感谢!
另外再次标注博客原地址  xiaorui.cc