执行结果如下:
total: 1
the number of goroutines: 2
这段代码通过启动两个 goroutine 对 total 进行加法操作,为防止出现数据竞争,对计算部分做了加锁保护,但并没有及时的解锁,导致 i = 1 的 goroutine 一直阻塞等待 i = 0 的 goroutine 释放锁。可以看到,退出时有 2 个 goroutine 存在,出现了泄露,total 的值为 1。
怎么解决?因为 Go 有 defer 的存在,这个问题还是非常容易解决的,只要记得在 Lock 的时候,记住 defer Unlock 即可。
示例如下:
mutex.Lock()
defer mutext.Unlock()
其他的锁与这里其实都是类似的。
WaitGroup
WaitGroup 和锁有所差别,它类似 Linux 中的信号量,可以实现一组 goroutine 操作的等待。使用的时候,如果设置了错误的任务数,也可能会导致阻塞,导致泄露发生。
一个例子,我们在开发一个后端接口时需要访问多个数据表,由于数据间没有依赖关系,我们可以并发访问,示例如下:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func handle() {
var wg sync.WaitGroup
wg.Add(4)
go func() {
fmt.Println("访问表1")
wg.Done()
}()
go func() {
fmt.Println("访问表2")
wg.Done()
}()
go func() {
fmt.Println("访问表3")
wg.Done()
}()
wg.Wait()
}
func main() {
defer func() {
time.Sleep(time.Second)
fmt.Println("the number of goroutines: ", runtime.NumGoroutine())
}()
go handle()
time.Sleep(time.Second)
}
执行结果如下:
the number of goroutines: 2
出现了泄露。再看代码,它的开始部分定义了类型为 sync.WaitGroup 的变量 wg,设置并发任务数为 4,但是从例子中可以看出只有 3 个并发任务。故最后的 wg.Wait() 等待退出条件将永远无法满足,handle 将会一直阻塞。
怎么防止这类情况发生?
我个人的建议是,尽量不要一次设置全部任务数,即使数量非常明确的情况。因为在开始多个并发任务之间或许也可能出现被阻断的情况发生。最好是尽量在任务启动时通过 wg.Add(1) 的方式增加。
示例如下:
...
wg.Add(1)
go func() {
fmt.Println("访问表1")
wg.Done()
}()
wg.Add(1)
go func() {
fmt.Println("访问表2")
wg.Done()
}()
wg.Add(1)
go func() {
fmt.Println("访问表3")
wg.Done()
}()
...
总结
大概介绍完了我认为的所有可能导致 goroutine 泄露的情况。总结下来,其实无论是死循环、channel 阻塞、锁等待,只要是会造成阻塞的写法都可能产生泄露。因而,如何防止 goroutine 泄露就变成了如何防止发生阻塞。为进一步防止泄露,有些实现中会加入超时处理,主动释放处理时间太长的 goroutine。









