runtime.SetFinalizer使用学习

我们知道Go是自带GC的语言,在代码中调用runtime.GC()可以显式触发GC,当内存资源不足时调用可以在该时间点释放内存,但此时,由于STW机制的存在,程序性能可能短暂下降。runtime.SetFinalizer可以在内存中对象被回收前,指定一些操作。

用法介绍

函数定义如下:

func SetFinalizer(obj interface{}, finalizer interface{})

阅读官方的注释,有几点需要注意:

1.obj必须是对象指针。
2.SetFinalizer使原本对象的回收延长到了两步,第一步是解除finalizer和obj对象的关联,另起一个协程执行finalizer。第二步gc时才真正回收obj对象。这造成了对象生命周期的延长,对于大量对象分配的高并发场景需要引起注意。
3.finalizer的执行顺序具有依赖关系。如果A指向B,两者都设置了finalizer,则如果A和B均不可达,则在GC时首先执行A的finalizer,然后回收A。之后才能执行B的finalizer。
4.禁止指针循环引用和SetFinalizer同时使用。考虑到第3点,如果多个指针构成循环引用,则无法确定finalizer的依赖关系,因而无法执行SetFinalizer,目标对象不能变成不可达状态,造成内存无法回收。

应用实例

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

定义cache为包内部结构体,成员变量有实际存储数据的items,再在Cache包装cache,作为导出结构体

type Cache struct {
	*cache
}

type cache struct {
	defaultExpiration time.Duration
	items             map[string]Item
	mu                sync.RWMutex
	onEvicted         func(string, interface{})
	janitor           *janitor
}

初始化一个带有定期清理间隔时长的cache如下:

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将会进行CG流程,首先执行stopJanitor函数,其作用是为内部的stop channel写入值,从而通知上一步的异步清理协程,使其退出。这样就实现了业务代码无感知的异步协程回收,是一种优雅的退出方式。

posted @ 2021-11-01 20:37  g2012  阅读(484)  评论(0)    收藏  举报