前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >Go: runtime.SetFinalizer 详解

Go: runtime.SetFinalizer 详解

作者头像
用户11547645
发布2025-03-07 16:18:54
发布2025-03-07 16:18:54
2800
代码可运行
举报
文章被收录于专栏:萝卜要加油萝卜要加油
运行总次数:0
代码可运行

注意,这是一篇旧文章,Golang可能会取消runtime.SetFinalizer,使用runtime.AddCleanup 替代。它解决了 runtime.SetFinalizer 一些痛点。具体内容可以参考我这篇文章:

Go 1.24: runtime.AddCleanup, 改进 runtime.SetFinalizer 的一些问题

如果我们希望在一个对象被gc之前,做一些资源释放的工作,我们可以使用 runtime.SetFinalizer。就像函数返回之前执行defer释放资源一样。比如下面的代码:

List1: example By runtime.SetFinalizer

代码语言:javascript
代码运行次数:0
复制

type MyStruct struct {  
    Name  string
    Other *MyStruct  
}

func main() {  
    x := MyStruct{Name: "X"}  
    runtime.SetFinalizer(&x, func(x *MyStruct) {  
       fmt.Printf("Finalizer for %s is called\n", x.Name)  
    })  
    runtime.GC()  
    time.Sleep(1 * time.Second)  
    runtime.GC()  
}

官方文档中对SetFinalizer的一些解释,主要含义是对象可以关联一个SetFinalizer函数, 当GC检测到unreachable对象有关联的SetFinalizer函数时,会执行关联的SetFinalizer函数, 同时取消关联。 这样当下一次GC的时候,对象重新处于unreachable状态并且没有SetFinalizer关联, 就会被回收。

仔细看文档,还有几个需要注意的点:

  • 即使程序正常结束或者发生错误, 但是在对象被 gc 选中并被回收之前,SetFinalizer 都不会执行, 所以不要在SetFinalizer中执行将内存中的内容flush到磁盘这种操作。
  • SetFinalizer 最大的问题是延长了对象生命周期。在第一次回收时执行 Finalizer 函数,且目标对象重新变成可达状态,直到第二次才真正 “销毁”。这对于有大量对象分配的高并发算法,可能会造成很大麻烦
  • 指针构成的 "循环引⽤" 加上 runtime.SetFinalizer 会导致内存泄露。

list 2: runtime.SetFinalizer memory leak

代码语言:javascript
代码运行次数:0
复制
// MyStruct 是一个简单的结构体,包含一个指针字段。  
type MyStruct struct {  
    Name  string
    Other *MyStruct  
}  

func main() {  
    x := MyStruct{Name: "X"}  
    y := MyStruct{Name: "Y"}  

    x.Other = &y  
    y.Other = &x  
    runtime.SetFinalizer(&x, func(x *MyStruct) {  
       fmt.Printf("Finalizer for %s is called\n", x.Name)  
    })  
    time.Sleep(time.Second)  
    runtime.GC()  
    time.Sleep(time.Second) 
    runtime.GC() 
}

x 永远不会被释放。正确的做法应该是, 在不需要使用 对象的时候,显式移除 Finalizer runtime.SetFinalizer(&x, nil)

实际应用

在业务代码中很少使用runtime.SetFinalizer (我没使用过)但是再Go源码中 有比较多的使用, 比如 net/http

代码语言:javascript
代码运行次数:0
复制
func (fd *netFD) setAddr(laddr, raddr Addr) {  
    fd.laddr = laddr  
    fd.raddr = raddr  
    runtime.SetFinalizer(fd, (*netFD).Close)  
}  

func (fd *netFD) Close() error {  
if fd.fakeNetFD != nil {  
return fd.fakeNetFD.Close()  
    }  
    runtime.SetFinalizer(fd, nil)  
return fd.pfd.Close()  
}

go-cache库提供了SetFinalizer的一种用法。

代码语言:javascript
代码运行次数:0
复制
func New(defaultExpiration, cleanupInterval time.Duration) *Cache {
    items := make(map[string]Item)
return newCacheWithJanitor(defaultExpiration, cleanupInterval, items)
}

func newCacheWithJanitor(de time.Duration, ci time.Duration, m map[string]Item) *Cache {
    c := newCache(de, m)
    C := &Cache{c}
if ci > 0 {
        runJanitor(c, ci)
        runtime.SetFinalizer(C, stopJanitor)
    }
return C
}

func runJanitor(c *cache, ci time.Duration) {
    j := &janitor{
        Interval: ci,
        stop:     make(chanbool),
    }
    c.janitor = j
go j.Run(c)
}

func stopJanitor(c *Cache) {
    c.janitor.stop <- true
}

func (j *janitor) Run(c *cache) {
    ticker := time.NewTicker(j.Interval)
for {
select {
case <-ticker.C:
            c.DeleteExpired()
case <-j.stop:
            ticker.Stop()
return
        }
    }
}

newCacheWithJanitor在ci参数大于0时,将开启后台协程,通过ticker定期清理过期缓存。一旦从stop chan中读到值,则异步协程退出。 stopJanitor为指向Cache的指针C定义了finalizer函数stopJanitor。一旦我们在业务代码中不再有指向Cache的引用时,c将会进行GC流程,首先执行stopJanitor函数,其作用是为内部的stop channel写入值,从而通知上一步的异步清理协程,使其退出。这样就实现了业务代码无感知的异步协程回收,是一种优雅的退出方式。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-02-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 萝卜要加油 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 实际应用
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档