Goroutine 泄漏

Dec 23, 2020 20:30 · 924 words · 2 minute read Golang

Goroutine 泄漏经常会导致 Go 程序内存泄漏。

下面的代码中每个 Goroutine 都从“输入”管道中接收值并将新的值发送到“输出”管道:

package main

import (
    "fmt"
    "runtime"
    "strings"
    "time"
)

func main() {
    // Capture starting number of goroutines.
    startingGs := runtime.NumGoroutine()

    names := []string{"foo", "bar", "baz"}
    processRecords(names)

    // Hold the program from terminating for 1 second to see
    // if any goroutines created by processRecords terminate.
    time.Sleep(time.Second)

    // Capture ending number of goroutines.
    endingGs := runtime.NumGoroutine()

    // Report the results.
    fmt.Println("========================================")
    fmt.Println("Number of goroutines before:", startingGs)
    fmt.Println("Number of goroutines after :", endingGs)
    fmt.Println("Number of goroutines leaked:", endingGs-startingGs)
}

func processRecords(records []string) {
    total := len(records)
    input := make(chan string, total)
    for _, record := range records {
        input <- record
    }
    // close(input) // What if we forget to close the channel?

    output := make(chan string, total)
    workers := runtime.NumCPU()
    for i := 0; i < workers; i++ {
        go worker(i, input, output)
    }

    for i := 0; i < total; i++ {
        result := <-output
        fmt.Printf("[result  ]: output %s\n", result)
    }
}

func worker(id int, input <-chan string, output chan<- string) {
    for v := range input {
        fmt.Printf("[worker %d]: input %s\n", id, v)
        output <- strings.ToUpper(v)
    }
    fmt.Printf("[worker %d]: shutting down\n", id)
}

我们来跑一下上面的代码:

$ go run main.go
[worker 4]: input foo
[worker 7]: input bar
[result  ]: output BAR
[worker 15]: input baz
[result  ]: output FOO
[result  ]: output BAZ
========================================
Number of goroutines before: 1
Number of goroutines after : 17
Number of goroutines leaked: 16

是不是感觉有很多 Goroutine 阻塞了。。。

input 是一个带缓冲的管道,足够容纳 records 切片中的所有字符串,所以 34 行处的 for 循环不会阻塞。这个管道将会在 Goroutine 之间徘徊。

40 行搞了一个简易的 Goroutine 池来从管道接收值。output 也是一个带缓冲的管道,每个 Goroutine 都会把值发到这来。Goroutines 与机器逻辑 CPU 的数量相等,循环下标还有 inputoutput 管道都会传递到 Goroutine 中。

再来看下 worker 的定义。51 行 Goroutine 利用 range 循环从 input 管道接收值,直到管道被关闭或者没有值为止。每次迭代都会将接收到的值输出,转换成大写并发送给 output 管道。

回到 processRecords 函数,45 行另一个循环会迭代直至将 output 管道中的值全都取出并输出。然后退出循环结束程序。

这段程序跑着看起来还行,但泄漏了很多 Goroutines。56 行代表了 worker 被关闭的这行代码是永远都不会执行到的。甚至 processRecords 函数都返回了,worker Goroutine 都还活着,正在 54 行处等待。因为程序从未关闭管道

想必你也注意到了我注释掉的那行代码 close(input),关闭管道代表再也没有数据能发进去了。

total := len(records)
    input := make(chan string, total)
    for _, record := range records {
        input <- record
    }
    close(input)

是可以在管道中还有值的时候就将其关闭的,此时管道只不过封死了入口,当被关闭的管道中的值完全取出后 range 也就到头了,使得 worker 能够结束循环。

$ go run main.go
[worker 15]: input bar
[worker 0]: input foo
[worker 0]: shutting down
[worker 12]: shutting down
[worker 3]: shutting down
[worker 13]: shutting down
[result  ]: output BAR
[worker 9]: shutting down
[worker 14]: shutting down
[worker 1]: shutting down
[worker 6]: shutting down
[worker 2]: shutting down
[worker 4]: shutting down
[worker 5]: shutting down
[worker 7]: input baz
[worker 7]: shutting down
[worker 10]: shutting down
[result  ]: output FOO
[worker 8]: shutting down
[result  ]: output BAZ
[worker 11]: shutting down
[worker 15]: shutting down
========================================
Number of goroutines before: 1
Number of goroutines after : 1
Number of goroutines leaked: 0

还有很多种 Goroutine 泄漏的情况,使用 Goroutine 要有始有终