goroutine 内存泄漏问题

11-17 4,319 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则该内存才会被回收。