golang:利用GC关闭协程,避免内存泄漏

go语言有完备的垃圾回收(gc)机制,但仍然可能发生内存泄漏,如下:

在函数test()内开启一个协程:

func test() {
    ......
    go func(){
        for {
            // do sth..
        }
    }()
    .....
}

如果for循环内没有return或break,即使test()函数结束,该协程也会继续运行,直到程序结束。
如果这不是我们的本意,就发生了协程泄漏。协程泄漏的同时也会导致内存泄漏,因为协程一直在运行,里边用到的内存无法被gc回收。
当然,只要小心一点,我们基本不会犯这种错误。

但还有更隐蔽的情况,比如:封装了一个具有定期清理到期key功能的map库,我们就叫它TtlMap
TtlMap在初始化时会开启一个协程,负责定期删除到期的key,代码框架如下:

type TtlMap struct {
    ...
}

func New() *TtlMap {
    map := &TtlMap{
        ...
    }

    //启动定期清理协程
    go map.clear()
    return map
}

func (m *TtlMap) clear() {
    for {
        //定期清理...
    }
}

有一定go语言编程经验的人能看得出,这个map库是有问题的:没有提供结束map.clear()协程的方法。
TtlMap作为一个局部变量时,定义它的函数结束后,由于clear()协程还在运行,变量无法被Gc回收,导致内存泄漏。
解决此问题的通用方法是,TtlMap中加一个chan,通过chan发信号来关闭clear()协程:

type TtlMap struct {
    ...
    stop chan bool
}

func New() *TtlMap {
    map := &TtlMap{
        ...
    }

    //启动定期清理协程
    go map.clear()
    return map
}

func (m *TtlMap) clear() {
    for {
        select {
        // 关闭
		case <-m.stop:
			return

        //定期清理...
		}
    }
}

func (m *TtlMap) Close() {
    m.stop <- true
}

这么做不太安全,当使用者忘记调用Close()方法关闭map,仍然会导致泄漏。
如何不调用Close,又能让Gc回收呢?可以在初始化时,把Ttlmap打包一层再返回:

type TtlMap struct {
    ...
    stop chan bool
}

// 包裹
type MapWarpper struct {
    *TtlMap
}

func New() *MapWarpper {
    map := &TtlMap{
        ...
    }
    go map.clear()

    // 包一层
    mw := &MapWarpper{map}
    
    // 重点在此:设置被回收时操作
    runtime.SetFinalizer(mw, onGarbageCollect)
    return mw
}

func (m *TtlMap) clear() {
    for {
        select {
        // 关闭
		case <-m.stop:
			return

        //定期清理...
		}
    }
}

func onGarbageCollect(m *MapWarpper) {
    m.stop <- true
}

SetFinalizer介绍
如果需要在一个对象object被从内存中移除前执行一些特殊操作,比如写日志等,可以通过调用以下方式调用函数来实现
runtime.SetFinalizer(obj, func(obj *typeObj))
在对象被GC进程选中并从内存中移除前,SetFinalizer都不会执行,即使程序正常结束或者发生错误

MapWarpper作为局部变量时,定义它的函数结束后,MapWarpper的生命周期已结束,Gc会将其回收。
Gc回收MapWarpper时执行了onGarbageCollect()函数,将Ttlmap的clear协程关闭,进而将Ttlmap回收。
这样,我们实现了一个安全的map库,由Gc自动关闭协程,回收内存,无需手动Close。


  • 问题1:不能直接给ttlmap设置SetFinalizer吗?
    答:不能。只要clear()协程一直在运行,Gc就无法选中ttlmap对象执行垃圾回收。
    实际上,给ttlmap设置了SetFinalizer后,如果clear()协程没有return,而是阻塞了,Gc仍然可以将ttlmap回收掉。

  • 问题2:使用了Warpper,却没有给Warpper设置SetFinalizer,Warpper还会被回收吗?
    答:经测试,不会被回收。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值