Featured image of post Go runtime.SetFinalizer

Go runtime.SetFinalizer

 

解析

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

This article was first published in the Medium MPP plan. If you are a Medium user, please follow me on Medium. Thank you very much.

List1: example By runtime.SetFinalizer

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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的一种用法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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(chan bool),
	}
	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写入值,从而通知上一步的异步清理协程,使其退出。这样就实现了业务代码无感知的异步协程回收,是一种优雅的退出方式。

Licensed under CC BY-NC-SA 4.0
最后更新于 Sep 04, 2024 19:33 CST
使用 Hugo 构建
主题 StackJimmy 设计