11-17 8,959 views
昨天晚上失眠一夜,一直在琢磨关于goroutine 内存泄漏问题。今天早晨突然开窍,将这个问题想明白了。在抛出bug之前先来说说事情的经过。昨天内部年终总结时,有一同事提到一个goroutine内存泄漏问题,优化后性能提升N多倍。当时我挺感兴趣的,所以开完会之后线下找到这哥们了解一下。大概的源码逻辑是这样的。
原始版本:
import ( "time" ) func main() { c := make(chan struct{}) go func() { time.Sleep(time.Second * 2) c <- struct{}{} }() go func() { select { case <-time.After(time.Second): fmt.Println("Timeout...") case <-c: fmt.Println("Done") } }() time.Sleep(time.Second * 5) }
这个很容易理解,由于c<-struct{}{}发送数据时由于阻塞了2秒。另外一个goroutine已经超时退出了。所以goroutine一直阻塞在那里,占用着内存不进行释放。如果该goroutine被调用的次数少还看不出来问题,关键的问题是该goroutine被大量的调用。所以会有大量的goroutine阻塞那里不会释放,虽说每个goroutine占用的资源只有2kB,但是数量多了自然占用的内存也就多了。所以该同事将同步方式改为异步方式。
他修改后:
import ( "time" ) func main() { c := make(chan struct{}, 1) go func() { time.Sleep(time.Second * 2) c <- struct{}{} }() go func() { select { case <-time.After(time.Second): fmt.Println("Timeout...") case <-c: fmt.Println("Done") } }() time.Sleep(time.Second * 5) }
这个我昨天一致没有明白,可能没有仔细考虑该问题。今天早晨又想了一下,异步通道将任务push到channel之后就去干其它事情了,当前的执行序不回被阻塞(除非异步通道满了)。又结合它的业务可以肯定异步通道不会满的。所以c <- struct{}{}之后当前的goroutine并不会被阻塞住,而是执行完就退出了。只是每次需要的c通道没有被关闭,这样的缺点就是不能保证数据的安全性。因为如果该通道没有关闭,在其它地方有使用该通道。如果内部有数据,就会产生数据安全的问题。我更习惯的做法是,关闭通道后再进行退出程序。
package main import ( "fmt" "time" ) func main() { c := make(chan struct{}) go func() { time.Sleep(time.Second * 2) _, ok := <-c if !ok { return } c <- struct{}{} }() go func() { select { case <-time.After(time.Second): fmt.Println("timeout...") close(c) case <-c: fmt.Println("done.") } }() time.Sleep(time.Second * 5) }
通过这个问题也纠正了我一个对内存回收问题的错误理解。之前一直理解当channel被关闭后,该通道的内存也会被释放。经过今天向师兄付光荣请教之后,以及自己通过GODEBUG=gctrace=1验证后。发现自己的理解是错误的,内存的回收跟被引用它的计数有关系,跟通道是否关闭没有半毛钱关系。当引用计数为0则该内存才会被回收。
以前还真没注意这问题.
的确很容易忽略,只有当goroutine量比较大时。可能看出来问题。