2.13 有哪些情况会导致协程泄露?

协程泄露是指,在程序运行过程中,有一些协程由于某些原因,无法正常退出。

协程的运行是需要占用内存和 CPU 时间的,一旦这种协程越来越多,会导致内存无端被浪费,CPU 时间片被占用,程序会越来越卡。

那会导致协程泄露的原因有哪些呢?

其实不用死记硬背,只要有可能导致程序阻塞的,都有可能会导致协程泄露。

那么会让程序阻塞的,无外乎:

  • 通道阻塞

  • 锁阻塞

  • 等待阻塞

1. 通道使用不当

通道和协程是 Go 的两大杀器,配合好了是非常香的,但要是没用好,就会造成协程泄露。

下面是常见的一些通道使用不当导致的协程泄露例子

发送了却没全接收

func main() {
    for i := 0; i < 4; i++ {
        queryAll()
        fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
    }
}

func queryAll() int {
    ch := make(chan int)
    for i := 0; i < 3; i++ {
        go func() { ch <- query() }()
        }
    return <-ch
}

func query() int {
    n := rand.Intn(100)
    time.Sleep(time.Duration(n) * time.Millisecond)
    return n
}

没发送却有人在接收

func main() {
    defer func() {
        fmt.Println("goroutines: ", runtime.NumGoroutine())
    }()

    var ch chan struct{}
    go func() {
        ch <- struct{}{}
    }()

    time.Sleep(time.Second)
}

初始化通道却没分配内存

func main() {
    defer func() {
        fmt.Println("goroutines: ", runtime.NumGoroutine())
    }()

    var ch chan int
    go func() {
        <-ch
    }()

    time.Sleep(time.Second)
}

2. 锁使用不当

加互斥锁后没有解锁

加了互斥锁后,若没有释放,其他 Goroutine 再想获取锁就会阻塞。

因此在加了互斥锁后,可以下意识加个 defer mutex.Unlock(),养成编码习惯

func main() {
    total := 0
    defer func() {
        time.Sleep(time.Second)
        fmt.Println("total: ", total)
        fmt.Println("goroutines: ", runtime.NumGoroutine())
    }()

    var mutex sync.Mutex
    for i := 0; i < 10; i++ {
        go func() {
            mutex.Lock()
            // 正常加锁后,可以下意识加个 defer mutex.Unlock()
            total += 1
        }()
    }
}

同步锁使用不当

如下例子中,wg.Add 的数量与 wg.Done 的数量不一致,就会导致 wg.Wait 阻塞。

func handle(v int) {
    var wg sync.WaitGroup
    wg.Add(5)
    for i := 0; i < v; i++ {
        wg.Done()
    }
    wg.Wait()
}

func main() {
    defer func() {
        fmt.Println("goroutines: ", runtime.NumGoroutine())
    }()

    go handle(3)
    time.Sleep(time.Second)
}

3. 慢等待未响应

在下面这个例子中,发送一个 http 请求的时候,如果网络非常的卡,导致这个请求一直没有收到响应,那么就会一直进行 for 循环,不断创建新的协程。

func main() {
    for {
        go func() {
            _, err := http.Get("https://www.xxx.com/")
            if err != nil {
                fmt.Printf("http.Get err: %v\n", err)
            }
            // do something...
    }()

    time.Sleep(time.Second * 1)
    fmt.Println("goroutines: ", runtime.NumGoroutine())
    }
}