Go 的并发模型与其他语言不同,虽说它简化了并发程序的开发难度,但如果不了解使用方法,常常会遇到 goroutine 泄露的问题。虽然 goroutine 是轻量级的线程,占用资源很少,但如果一直得不到释放并且还在不断创建新协程,毫无疑问是有问题的,并且是要在程序运行几天,甚至更长的时间才能发现的问题。
泄露的原因大多集中在:
- Goroutine 内正在进行 channel/mutex 等读写操作,但由于逻辑问题,某些情况下会被一直阻塞。
- Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
- Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。
Channel 使用不当
发送不接收
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
}
输出结果如下:
goroutines: 3
goroutines: 5
goroutines: 7
goroutines: 9
接收不发送
func main() {
defer func() {
fmt.Println("goroutines: ", runtime.NumGoroutine())
}()
var ch chan struct{}
go func() {
ch <- struct{}{}
}()
time.Sleep(time.Second)
}
输出结果如下:
goroutines: 2
nil channel
func main() {
defer func() {
fmt.Println("goroutines: ", runtime.NumGoroutine())
}()
var ch chan int
go func() {
// fatal error: all goroutines are asleep - deadlock!
<-ch
}()
time.Sleep(time.Second)
}
输出结果如下:
goroutines: 2
channel 如果忘记初始化,那么无论是读,还是写操作,都会造成阻塞。 正确的姿势如下:
ch := make(chan int)
go func() {
<-ch
}()
ch <- 0
time.Sleep(time.Second)
奇怪的慢等待
func main() {
for {
go func() {
// Go 语言中默认的 http.Client 是没有设置超时时间
_, err := http.Get("https://www.xxx.com/")
if err != nil {
fmt.Printf("http.Get err: %v\n", err)
}
fmt.Println("do something...")
}()
time.Sleep(time.Second * 1)
fmt.Println("goroutines: ", runtime.NumGoroutine())
}
}
输出结果如下:
goroutines: 5
goroutines: 9
goroutines: 13
goroutines: 17
goroutines: 21
goroutines: 25
...
正确的姿势如下:
httpClient := http.Client{
Timeout: time.Second * 15,
}
互斥锁忘记解锁
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()
total += 1
}()
}
}
输出结果如下:
total: 1
goroutines: 10
正确的姿势如下:
var mutex sync.Mutex
for i := 0; i < 10; i++ {
go func() {
mutex.Lock()
defer mutex.Unlock()
total += 1
}()
}
同步锁使用不当
func handle(v int) {
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < v; i++ {
fmt.Println("脑子进水了")
wg.Done()
}
wg.Wait()
}
func main() {
defer func() {
fmt.Println("goroutines: ", runtime.NumGoroutine())
}()
go handle(3)
time.Sleep(time.Second)
}
输出结果如下:
脑子进水了
脑子进水了
脑子进水了
goroutines: 2
正确的姿势如下:
var wg sync.WaitGroup
for i := 0; i < v; i++ {
wg.Add(1)
defer wg.Done()
fmt.Println("脑子进水了")
}
wg.Wait()
排查方法
可以调用 runtime.NumGoroutine 方法来获取 Goroutine 的运行数量,进行前后对比,就能知道有没有泄露了。但在业务服务的运行场景中,Goroutine 内导致的泄露,更多的是使用 PProf:
import (
"net/http"
_ "net/http/pprof"
)
http.ListenAndServe("localhost:6060", nil))