[golang] Goroutine 泄露

1,632 阅读2分钟

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))

参考