跳过正文

Golang之内存泄漏

·195 字·1 分钟
Chuck Chan
作者
Chuck Chan
分享技术、思考与生活

Golang之内存泄漏
#

内存泄漏
#

内存泄漏说白了就是分配的内存(或者变量)不再使用,但是并没有被gc回收,而是继续占用内存。在Golang中内存泄漏大部分都跟channel的不正确使用有关。泄漏的原因是goroutine操作channel后,处于发送阻塞或者接收阻塞状态,而channel处于满或者空的状态,一直得不到改变。同时,垃圾回收器也不会回收此类资源,进而导致goroutine会处于一个一直等待的队列中,不见天日。

场景分析
#

案例1: channel没有接收者
#

下面这段程序展示了并发地向3个站点请求资源,接收者只接收最快返回的那个。

func goroutineLeak1() string {
	responses := make(chan string) //正确姿势:responses := make(chan string, 3)
  //3个发送者
	go func {responses <- request("asia.gopl.io")}()
	go func {responses <- request("europe.gopl.io")}()
	go func {responses <- request("americas.gopl.io")}()
  //1个接收者
	return <- responses
}

goroutineLeak1能正确地接收到最快返回的站点资源,但两个慢的goroutien会因为没有人接收而永远卡住,造成这两个goroutine永远没法被回收。正确的姿势是创建一个带3个buffer的channel,可以回收两个返回比较慢的goroutine。

案例2: channel没有发送者
#

下面这段程序模拟生产者与消费者。

func goroutineLeak2() {
	ch := make(chan int)
	//10接收者
	for i := 0; i < 10; i++ {
		go func(i int) {
			fmt.Printf("%d号消费者 接收到消息:%d", i, <-ch)
		}(i)
	}

	//1个发送者
	go func() {  //正确姿势: 需要生产10个消息
		ch <- rand.Int()
	}()
}

goroutineLeak2中创建10个goroutine来接收消息,但只有一个生产者生产消息,接收者goroutine会因为没有消费消息而阻塞,也没法被回收。正确的姿势是生产者创建10条消息,使得接收者能够完成消费。

案例3: timer使用错误
#

time.After每次都会创建一个定时器,定时器未到触发时间,该定时器不会被gc回收,从而导致临时性的内存泄漏。

func goroutineLeak3() {
	ch := make(chan string)
	//接收数据
	go func() {
		for {
			select {
			case <-time.After(3 * time.Second):
				fmt.Println("request timeout")
			case data := <-ch:
				fmt.Println("收到数据: ", data)
			}
		}
	}()

	//模拟发送数据
	for i := 0; i < 10000; i++ {
		ch <- "ok"
		time.Sleep(time.Second)
	}
}

正确姿势是创建timer定时器,每次需要启动定时器的时候,使用Reset方法重置定时器,这样就不用每次都要创建新的定时器了。

func goroutineLeak3() {
	ch := make(chan string)
	go func() {
		timer := time.NewTimer(3 * time.Second)
		defer timer.Stop()
		for {
			timer.Reset(3 * time.Second)
			select {
			case <-timer.C:
				fmt.Println("request timeout")
			case data := <-ch:
				fmt.Println("收到数据: ", data)
      default:
			}
		}
	}()

	for i := 0; i < 10000; i++ {
		ch <- "ok"
		time.Sleep(time.Second)
	}
}