介绍 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
泄漏预防
参考链接