如果没了解过优雅方案,可以看一下这个简书如何优雅地关闭Go channel。
在我看来这些方案不优雅!不灵活!不实用!比如1个消费者N个生产者的情况下,如果要求消费者要把channel消费完该怎么处理?
要做到优雅,我觉得有3点:
- 消费者不关闭channel
- 消费者必须把channel内的数据消费完
- channel必须在没有生产者使用时被关闭
我们只要做好第3点,第1第2点唾手可得。
现在来看看更优雅的方案,解决问题的思路就是维护一个生产者的数量sendersNum,当sendersNum为0的时候关闭channel。
import ( "math/rand" "sync" "time" ) func main() { // 并发访问sendersNum需要锁 l := &sync.Mutex{} // 生产者的数量 sendersNum := 0 // data channel dataChan := make(chan int, 5) // 发起10个生产者 for i := 0; i < 10; i++ { l.Lock() sendersNum++ l.Unlock() go func() { for { x := rand.Intn(20) // 如果x<5, 生产者退出 if x < 5 { l.Lock() sendersNum-- l.Unlock() return } else {// 否则x>=5, 发送数据到dataChan dataChan <- x } } }() } // 发起10个消费者 for i := 0; i < 10; i++ { go func() { // 消费者要做的事情就是不断读取直到ok字段为false for { x, ok := <- dataChan if !ok { return } else { println(x) } } }() } // 发起一个关闭者,是该方案的核心 go func() { // 关闭者每隔1秒醒来检查sendersNum // 当sendersNum为0的时候,说明没有生产者再使用channel了,可以放心关闭 // 使用time.Sleep是一个粗糙的实现,但并不妨碍我们说明问题,也可以使用更优雅的条件变量 for { time.Sleep(1*time.Second) l.Lock() if sendersNum == 0 { close(dataChan) return } l.Unlock() } }() // 避免主协程结束导致子协程提前结束,也是一个粗糙的实现 time.Sleep(10*time.Second) }
这是多个消费者多个生产者的情况。如果只有一个发送者,可以将关闭者优化掉,在发送者退出时直接关闭channel。如果只有一个消费者,虽然也可以优化掉关闭者,但还是建议采用这个关闭者的方案。
这个方案是灵活的,唯一的要求就是维护sendersNum,发起生产者的时候sendersNum++,生产者退出的时候sendersNum-- 。至于生产者为什么会退出不做要求,可以是x不满足条件,也可以是消费者关闭了一个stopCh,或者是发生了超时事件。
当一个生产者需要向多个channel发送数据时,决定停止向某个channel发送数据,将该channel的sendersNum-- 就可以了,然后继续生产其他channel的数据了。
关闭者也可以同时检查多个channel的sendersNum。
这个方案也是实用的。有多少生产者会向channel发送数据是个重要的信息,sendersNum不仅可以用来关闭channel,还可以做为监控系统的一个指标。
在应用上还是以实用为主,优雅关闭方案只是提供了一种选择,defer recovery的方案也是一种选择,还有库中用得比较多的用closed字段来维护channel的方案。
最后,大四刚开学,实战经验少,轻喷。