Goroutine 泄漏
Dec 23, 2020 20:30 · 924 words · 2 minute read
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 的数量相等,循环下标还有 input
和 output
管道都会传递到 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 要有始有终。