解密 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 很有用,但有幾個關鍵點需要注意:

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