解密 Go runtime-SetFinalizer 的使用
如果我們想在對象 GC 之前釋放一些資源,可以使用 returns.SetFinalizer。這就像在函數返回前執行 defer 來釋放資源一樣。例如:
1:使用 runtime.SetFinalizer
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()
}
官方文檔 [1] 解釋說,SetFinalizer 會將終結器函數與對象關聯起來。當垃圾收集器(GC)檢測到一個不可訪問的對象有一個關聯的終結器時,它會執行終結器並解除關聯。如果該對象是不可到達的,並且不再有關聯的終結器,那麼它將在下一個 GC 週期被收集。
重要考慮因素
雖然 runtime.SetFinalizer 很有用,但有幾個關鍵點需要注意:
-
延遲執行:SetFinalizer 函數在對象被選中進行垃圾回收之前不會執行。因此,應避免將 SetFinalizer 用於將內存中的內容刷新到磁盤等操作。
-
擴展對象生命週期:SetFinalizer 會無意中延長對象的生命週期。終結器函數會在第一個 GC 循環期間執行,目標對象可能會再次變得可觸及,從而延遲其最終銷燬。這在具有大量對象分配的高併發算法中可能會造成問題。
-
循環引用的內存泄漏:與循環引用一起使用 runtime.SetFinalizer 可能會導致內存泄漏。
2:運行時. SetFinalizer 的內存泄漏
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 永遠不會被釋放。正確的方法是在不再需要該對象時,顯式地移除終結器:runtime.SetFinalizer(&x, nil)。
實際應用
雖然 runtime.SetFinalizer 很少在業務代碼中使用(我從未使用過),但它在 Go 源代碼本身中使用得更爲普遍。例如,考慮一下 net/http 包中的以下用法:
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[2] 還展示了 SetFinalizer 的一個用法:
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 定期清理過期的緩存條目。一旦從停止通道讀取到一個值,異步程序就會退出。
stopJanitor 函數定義了 Cache 指針 C 的終結器。當業務代碼中不再引用 Cache 時,GC 進程會觸發 stopJanitor 函數,並向內部 stop 通道寫入一個值。這將通知異步清理 goroutine 退出,從而提供了一種優雅且與業務無關的資源回收方式。
參考資料
[1]
SetFinalizer Doc: https://pkg.go.dev/runtime#SetFinalizer
[2]
go-cache: https://github.com/patrickmn/go-cache/blob/46f407853014144407b6c2ec7ccc76bf67958d93/cache.go#L1123
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Z5JS0nAeDupT_husLv1RGQ