介绍 goroutine 泄漏的相关内容,主要讨论以下三个部分:
goroutine 泄漏判断
goroutine 泄漏分类
goroutine 泄漏预防
目录 Table of Contents
前情提要
goroutine 泄漏指的是因为编码中的陷阱使得 goroutine 无法正常释放而造成的内存泄漏,甚至导致内存溢出或最终程序崩溃。
本文将讨论 goroutine泄漏的判断、分类和预防,如有错漏,欢迎指出 ;P
goroutine泄漏判断
runtime.NumGoroutine
runtime.NumGoroutine 可以返回正在运行中的 goroutine 数量(包括 main goroutine 和 normal goroutine)。
1 2 3 4 5 6 7 8 9 10
| import( "fmt" "runtime" )
fmt.Println(runtime.NumGoroutine())
|
runtime/pprof
runtime/pprof 可以(以写入的形式)返回 goroutine 的运行数量和堆栈信息。
1 2 3 4 5 6 7 8 9 10
| import ( "os" "runtime/pprof" )
pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
|
http/net/pprof
http/net/pprof 可以(以访问的形式)返回 goroutine 的运行数量、堆栈信息和其它资源信息。
1 2 3 4 5 6 7 8 9 10 11 12
| import ( "net/http" _ "net/http/pprof" )
http.ListenAndServe("localhost:6060", nil)
|
gops
gops 支持列出当前环境下的进程信息。
在 .go 中:
1 2 3 4 5 6 7 8 9 10 11 12
| import ( "log" "github.com/google/gops/agent" )
if err := agent.Start(); err != nil { log.Fatalln(err) }
|
在 terminal 中:
1 2 3 4 5
| $ gops
$ gops stats PID
$ gops stack PID
|
leaktest
leaktest 将泄漏检测过程加入到自动化测试中去。
1 2 3 4 5 6 7 8 9 10 11
| import ( "github.com/fortytw2/leaktest" )
defer leaktest.Check(t) defer leaktest.CheckTimeout(t, time.Second) defer leaktest.CheckContext(ctx, t)
|
goroutine 泄漏分类
无退出的计算循环
- 对于没有函数调用,纯循环计算的
G,runtime 无法实行抢占;
- 显然,如果没有退出机制且程序常驻的话,每次启动的
goroutine 都得不到释放,就会发生 goroutine 泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "fmt" "runtime" "time" )
func test() { for { fmt.Println("Testing...") } }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
go test() }
|
不结束的I/O请求
- 对于 I/O 请求,
runtime 无法实行抢占;
- 如果 I/O 请求一直处于等待期间,该
goroutine 则无法释放,出现泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package main
import ( "bufio" "fmt" "os" "runtime" "time" )
func test() { input := bufio.NewScanner(os.Stdin) if input.Scan() { text := input.Text() fmt.Println(text) } }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
go test() }
|
channel 引起的泄漏
只发送不接收
- 上游生产速度远远大于下游消费速度,阻塞的
goroutine 会一直在 channel 的发送等待队列。
- 无缓冲
channel 没有接收就会阻塞,有缓冲 channel 缓冲满了就会阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| package main
import ( "fmt" "math/rand" "runtime" "time" )
func query() int { n := rand.Intn(100) return n }
func queryAll() int { ch := make(chan int) go func() { ch <- query() }() go func() { ch <- query() }() go func() { ch <- query() }() return <-ch }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
queryAll() }
|
只接收不发送
- 下游消费速度远远大于上游生产速度,阻塞的
goroutine 会一直在 channel 的接收等待队列。
- 无缓冲
channel 仍然接收就会阻塞,有缓冲 channel 缓冲空了就会阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| package main
import ( "fmt" "runtime" "time" )
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
ch := make(chan bool) go func() { ch <- true }() }
|
空 channel
- 向
nil channel 发送和接收数据都会导致阻塞,只进行声明而不初始化 channel 容易出现该类泄漏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package main
import ( "fmt" "runtime" "time" )
func test() { var ch chan bool <-ch }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
go test() }
|
空 select
select{} 永远无法响应导致协程阻塞,一般不会出现这种情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package main
import ( "fmt" "runtime" "time" )
func test() { select {} }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
go test() }
|
sync 引起的泄漏
Mutex 忘记解锁
- 有一个
goroutine 加锁忘了解锁,另一个 goroutine 竞争锁会失败,由此这个 goroutine 将一直地阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package main
import ( "fmt" "runtime" "sync" "time" )
func test() { var mutex sync.Mutex
for i := 1; i <= 2; i++ { go func() { mutex.Lock()
fmt.Println("goroutine index:", i) }() } }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
go test() }
|
WaitGroup 计数错误
WaiteGroup 的 Add 和 Done 数量不对应将引起 Wait 的等待退出条件永远无法满足,从而阻塞协程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package main
import ( "fmt" "runtime" "sync" "time" )
func test() { var wg sync.WaitGroup
wg.Add(2)
go func() { wg.Done() }()
wg.Wait() }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
go test() }
|
Cond 没发信号
Wait 方法阻塞等待条件变量满足条件,如果没有 Signal 或者 Broadcast,Wait 将会一直不能唤醒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| package main
import ( "fmt" "runtime" "sync" "time" )
func test() { cond := sync.NewCond(&sync.Mutex{})
condition := false
go func() { cond.L.Lock()
condition = true
cond.L.Unlock() }()
cond.L.Lock() for !condition { cond.Wait() } cond.L.Unlock() }
func main() { defer func() { time.Sleep(time.Second) fmt.Println("NumGoroutine:", runtime.NumGoroutine()) }()
go test() }
|
goroutine泄漏预防
参考链接