美文网首页
go channel 使用注意总结

go channel 使用注意总结

作者: wayyyy | 来源:发表于2022-08-08 00:48 被阅读0次

原文出自:用 channel 把 Go 程序写崩的三种姿势,你集齐过吗?

死锁问题
  • 只有生产者或者只有消费者

    // 只有生产者,没有消费者
    func f1() {
        ch := make(chan int)
        ch <- 1
    }
    
    // 只有消费者,没有生产者
    func f2() {
        ch := make(chan int)
        <-ch
    }
    
  • 生产者和消费者出现在同一个 goroutine 中

    func f2() {
        ch := make(chan int)
         ch <- 1  // 消费者没有执行到,会一直阻塞在这里
        <-ch  // 消费者
    }
    
goroutine 泄漏

错误地使用 channel 会导致 goroutine 泄漏,进而导致内存泄漏。

比如:生产者/消费者 所在的 goroutine 已经退出,而其对应的 消费者/生产者 所在的 goroutine 会永远阻塞住,直到进程退出

  • 生产者阻塞导致泄漏
    func leak1() {
        ch := make(chan int)
        // g1
        go func() {
            time.Sleep(2 * time.Second) // 模拟 io 操作
            ch <- 100                   // 模拟返回结果
        }()
    
        // g2
        // 阻塞住,直到超时或返回
        select {
        case <-time.After(500 * time.Millisecond):
            fmt.Println("timeout! exit...")
        case result := <-ch:
            fmt.Printf("result: %d\n", result)
        }
    }
    
    这里用 goroutine g1 来模拟IO操作,主 goroutine g2 来模拟客户端的处理逻辑。
  • 消费者阻塞导致泄漏
    如果生产者不继续生产,消费者所在的 goroutine 也会阻塞住,不会退出。

    func leak2() {
        ch := make(chan int)
    
        // 消费者 g1
        go func() {
            for result := range ch {
                fmt.Printf("result: %d\n", result)
            }
        }()
    
        // 生产者 g2
        ch <- 1
        ch <- 2
        time.Sleep(time.Second)  // 模拟耗时
        fmt.Println("main goroutine g2 done...")
    }
    

    这种情况下,只需要增加 close(ch) 的操作即可,for-range 操作在收到 close 的信号后会退出,goroutine 不再阻塞,能够被回收。

  • 如何预防泄漏?
    创建 goroutine 时就要想清楚它什么时候被回收,包括:

    1. 当 goroutine 退出时,需要考虑它使用的 channel 有没有可能阻塞对应的生产者、消费者的 goroutine
    2. 尽量使用 buffered channel使用 buffered channel 能减少阻塞发生、即使疏忽了一些极端情况,也能降低 goroutine 泄漏的概率
panic
  • 向已经 close 掉的 channel 继续发送数据

    func p1() {
        ch := make(chan int, 1)
        close(ch)
        ch <- 1
    }
    

    但在实际开发过程中,处理多个 goroutine 之间协作时,可能存在一个 goroutine 已经 close 掉 channel 了,另外一个不知道,也去 close 一下,就会 panic 掉。

    
    

    万恶之源就是在 go 语言里,你是无法知道一个 channel 是否已经被 close 掉的,所以在尝试做 close 操作的时候,就应该做好会 panic 的准备……

  • 多次 close 同一个 channel
    同上,在尝试往 channel 里发送数据时,就应该考虑:

    • 这个channel 已经关了吗?
    • 这个channel 什么时候,在哪个 goroutine 里关呢?
    • 谁来关呢?还是干脆不关?
如何优雅地 close channel
  • 需要检查channel 是否关闭?
    刚遇到上面说的 panic 问题时,我也试过去找一个内置的 closed 函数来检查关闭状态,结果发现,并没有这样一个函数……

    那么,如果有这样的函数,真能彻底解决 panic 的问题么?答案是不能。因为 channel 是在一个并发的环境下去做收发操作,就算当前执行 closed(ch) 得到的结果是 false,还是不能直接去关,例如如下 yy 出来的代码:

    if !closed(ch) {  // 返回 false
        // 在这中间出了幺蛾子!
        close(ch)  // 还是 panic 了……
    }
    
  • 需要 close 吗?

    除非必须关闭 chan,否则不要主动关闭。关闭 chan 最优雅的方式,就是不要关闭 chan~

    当一个 chan 没有 sender 和 receiver 时,即不再被使用时,GC 会在一段时间后标记、清理掉这个 chan。
    那么什么时候必须关闭 chan 呢?比较常见的是将 close 作为一种通知机制,尤其是生产者与消费者之间是 1:M 的关系时,通过 close 告诉下游:我收工了,你们别读了。

  • 谁来关?
    chan 关闭的原则:

    1、Don't close a channel from the receiver side 不要在消费者端关闭 chan
    2、Don't close a channel if the channel has multiple concurrent senders 有多个并发写的生产者时也别关

    只要我们遵循这两条原则,就能避免两种 panic 的场景,即:向 closed chan 发送数据,或者是 close 一个 closed chan。

    按照生产者和消费者的关系可以拆解成以下几类情况:

    1. 一写一读:生产者关闭即可
    2. 一写多读:生产者关闭即可,关闭时下游全部消费者都能收到通知
    3. 多写一读:多个生产者之间需要引入一个协调 channel 来处理信
    4. 多写多读:与 3 类似,核心思路是引入一个中间层以及使用 try-send 的套路来处理非阻塞的写入:

参考资料:
1、Golang channel 死锁的几种情况以及例子
2、How to Gracefully Close Channels
3、深入解析 Goroutine 泄露的场景:channel 发送者

相关文章

网友评论

      本文标题:go channel 使用注意总结

      本文链接:https://www.haomeiwen.com/subject/rzwvwrtx.html