• 面试官:谈谈 Go goroutine 泄露的场景


    大家好,我是木川

    一、什么是 goroutine 泄露

    在 Go 中,goroutine 泄露是指创建的 goroutine 没有被正确地关闭或管理,导致它们在程序运行过程中无法被回收,最终导致资源浪费和潜在的性能问题。以下是一些常见的导致 goroutine 泄露的场景

    常见泄露原因如下:

    • goroutine 内进行channel/mutex 等读写操作被一直阻塞。

    • goroutine 内的业务逻辑进入死循环,资源一直无法释放。

    • goroutine 内的业务逻辑进入长时间等待,有不断新增的 goroutine 进入等待

    二、泄露场景

    如果输出的 goroutines 数量是在不断增加的,就说明存在泄漏

    nil channel

    channel 如果忘记初始化,那么无论你是读,还是写操作,都会造成阻塞。

    1. func block1() {
    2.  var ch chan int // 未使用make初始化
    3.  for i := 0; i < 10; i++ {
    4.   go func() {
    5.    <-ch
    6.   }()
    7.  }
    8. }
    9. func main() {
    10.  fmt.Println("before goroutines: ", runtime.NumGoroutine())
    11.  block1()
    12.  time.Sleep(time.Second * 1)
    13.  fmt.Println("after goroutines: ", runtime.NumGoroutine())
    14. }

    输出结果:

    1. before goroutines:  1
    2. after goroutines:  11

    channel 发送未接收

    channel 发送数量 超过 channel接收数量,就会造成阻塞

    1. func block2() {
    2.  ch := make(chan int)
    3.  for i := 0; i < 10; i++ {
    4.   go func() {
    5.    ch <- 1 // 没有接收者
    6.   }()
    7.  }
    8. }
    9. func main() {
    10.  fmt.Println("before goroutines: ", runtime.NumGoroutine())
    11.  block2()
    12.  time.Sleep(time.Second * 1)
    13.  fmt.Println("after goroutines: ", runtime.NumGoroutine())
    14. }

    输出结果:

    1. before goroutines:  1
    2. after goroutines:  11

    channel 接收未发送

    channel 接收数量 超过 channel发送数量,也会造成阻塞

    1. func block3() {
    2.  ch := make(chan int)
    3.  for i := 0; i < 10; i++ {
    4.   go func() {
    5.    <-ch // 没有发送者
    6.   }()
    7.  }
    8. }
    9. func main() {
    10.  fmt.Println("before goroutines: ", runtime.NumGoroutine())
    11.  block3()
    12.  time.Sleep(time.Second * 1)
    13.  fmt.Println("after goroutines: ", runtime.NumGoroutine())
    14. }

    输出结果:

    1. before goroutines:  1
    2. after goroutines:  11

    资源连接未关闭

    比如文件打开或http连接未正常关闭,比如 http 未调用resp.Body.Close() ,goroutine不会退出

    1. func requestWithNoClose() {
    2.  _, err := http.Get("https://www.baidu.com")
    3.  if err != nil {
    4.   fmt.Println("error occurred while fetching page, error: %s", err.Error())
    5.  }
    6. }
    7. func requestWithClose() {
    8.  resp, err := http.Get("https://www.baidu.com")
    9.  if err != nil {
    10.   fmt.Println("error occurred while fetching page, error: %s", err.Error())
    11.   return
    12.  }
    13.  defer resp.Body.Close()
    14. }
    15. func block4() {
    16.  for i := 0; i < 10; i++ {
    17.   wg.Add(1)
    18.   go func() {
    19.     defer wg.Done()
    20.     requestWithNoClose()
    21.   }()
    22.  }
    23. }
    24. var wg = sync.WaitGroup{}
    25. func main() {
    26.  fmt.Println("before goroutines: ", runtime.NumGoroutine())
    27.  block4()
    28.  wg.Wait()
    29.  fmt.Println("after goroutines: ", runtime.NumGoroutine())
    30. }

    输出结果:

    1. before goroutines:  1
    2. after goroutines:  21

    一般发起http请求时,需要确保关闭body

    defer resp.Body.Close()

    互斥锁忘记解锁

    第一个协程获取 sync.Mutex 加锁了,但是他可能在处理业务逻辑,又或是忘记 Unlock 了。因此导致后面的协程想加锁,却因锁未释放被阻塞了

    1. package main
    2. import (
    3.  "fmt"
    4.  "runtime"
    5.  "sync"
    6.  "time"
    7. )
    8. func block5() {
    9.  var mutex sync.Mutex
    10.  for i := 0; i < 10; i++ {
    11.   go func() {
    12.    mutex.Lock()
    13.   }()
    14.  }
    15. }
    16. func main() {
    17.  fmt.Println("before goroutines: ", runtime.NumGoroutine())
    18.  block5()
    19.  time.Sleep(1 * time.Second)
    20.  fmt.Println("after goroutines: ", runtime.NumGoroutine())
    21. }

    输出结果:

    1. before goroutines:  1
    2. after goroutines:  11

    sync.WaitGroup使用不当

    由于 wg.Add 的数量与 wg.Done 数量并不匹配,因此在调用 wg.Wait 方法后,触发死锁检测器并导致程序崩溃

    1. package main
    2. import (
    3.  "fmt"
    4.  "sync"
    5. )
    6. func main() {
    7.  var wg sync.WaitGroup
    8.  wg.Add(1// 增加计数器
    9.  go func() {
    10.   // 注意:没有调用 wg.Done()
    11.   fmt.Println("Goroutine executed")
    12.  }()
    13.  // 主程序等待,但计数器没有被适当减少,触发死锁检测
    14.  wg.Wait()
    15.  fmt.Println("Main function exiting")
    16. }

    在这个示例中,Add 增加了计数器,但在 goroutine 中没有调用 Done 减少计数器,因此计数器永远不会减少到零,这会触发死锁检测器并导致程序崩溃。

    为了避免这种情况,确保在每个启动的 goroutine 中都使用 Done 减少计数器,以便计数器最终减少到零,并允许程序正常退出。这是一种良好的并发编程实践。

    无限循环

    如果一个 goroutine 进入无限循环而没有退出的机制,它会一直运行下去,直到程序结束。

    1. func block7() {
    2.  for i := 0; i < 10; i++ {
    3.   go func() {
    4.    for {
    5.     // 无限循环
    6.    }
    7.   }()
    8.  }
    9. }
    10. func main() {
    11.  fmt.Println("before goroutines: ", runtime.NumGoroutine())
    12.  block7()
    13.  time.Sleep(1 * time.Second)
    14.  fmt.Println("after goroutines: ", runtime.NumGoroutine())
    15. }

    输出结果:

    1. before goroutines:  1
    2. after goroutines:  11

    三、如何排查

    单个函数:调用 runtime.NumGoroutine 方法来打印 执行代码前后 goroutine 的运行数量,进行前后比较,就能知道有没有泄露了。

    生产/测试环境:使用pprof实时监测 goroutine 的数量

    最后给自己的原创 Go 面试小册打个广告,如果你从事 Go 相关开发,欢迎扫码购买,目前 10 元买断,加下面的微信发送支付截图额外赠送一份自己录制的 Go 面试题讲解视频

    0a3ffa89e6a972ed900afaa0f34a7bbc.jpeg

    6e9da71ed649226961a3d889c6d0476c.png

    如果对你有帮助,帮我点一下在看或转发,欢迎关注我的公众号

  • 相关阅读:
    Linux系统安装MongoDB流程
    SpringBoot整合Redis实现常用功能
    创建简单的 Docker 数据科学映像
    让代码变得优雅简洁的神器:Java8 Stream流式编程
    为什么低碳水饮食对减肥有效?给你科学的解释
    雅思口语同替高分表达
    Apache Kafka 可视化工具调研
    比特币减半后:见证历史性暴涨吗?
    Java实现桥接模式(设计模式 五)
    pytorch 入门(一)
  • 原文地址:https://blog.csdn.net/caspar_notes/article/details/133224049