編程學(xué)習(xí)網(wǎng) > 編程語言 > go > Go 中的 SetFinalizer 有什么用?怎么實(shí)現(xiàn)的?
2020
04-10

Go 中的 SetFinalizer 有什么用?怎么實(shí)現(xiàn)的?



這篇文章基于 Go-1.12 版本

Go runtime 提供了一種允許開發(fā)者將一個函數(shù)與一個變量綁定的方法 runtime.SetFinalizer,被綁定的變量從它無法被訪問時就被垃圾回收器視為待回收狀態(tài)。這個特性引起了高度的爭論,但本文并不打算參與其中,而是去闡述這個方法的具體實(shí)現(xiàn)。

無保障性

舉一個使用了 Finalizer 的例子

package main import (
 "fmt"  "math/rand"  "runtime"  "strconv"  "time" ) type Foo struct {
 a int } func main() {
 for i := 0; i < 3; i++ {
  f := NewFoo(i)
  println(f.a)
 }

 runtime.GC()
} //go:noinline func NewFoo(i int) *Foo {
 f := &Foo{a: rand.Intn(50)}
 runtime.SetFinalizer(f, func(f *Foo) {
  fmt.Println(`foo ` + strconv.Itoa(i) + ` has been garbage collected`)
 })

 return f
}

這段程序?qū)谶@個循環(huán)中創(chuàng)建三個 struct 的的實(shí)例,并將每個實(shí)例都綁定一個 finalizer。之后垃圾回收器將會被調(diào)用,并回收之前創(chuàng)建的實(shí)例。運(yùn)行這個程序,將會給到我們?nèi)缦螺敵觯?

31
37
47

如我們所見,finalizers 并沒有被調(diào)用,runtime 的文檔解釋這一點(diǎn):

在程序無法獲取到一個 obj 所指向的對象后的任意時刻,finalizer 被調(diào)度運(yùn)行,且無法保證 finalizer 運(yùn)行在程序退出之前。因此一般情況下,因此它們僅用于在長時間運(yùn)行的程序上釋放一些與對象關(guān)聯(lián)的非內(nèi)存資源。

在調(diào)用 finalizer 之前,runtime 不提供有關(guān)延遲的任何保證。讓我們試著去修改我們的程序,通過在調(diào)用垃圾回收器之后添加一個一秒的 sleep:

31
37
47
foo 1 has been garbage collected
foo 0 has been garbage collected

現(xiàn)在我們的 finalizer 已經(jīng)被調(diào)用了,然而,它們其中一個消失了。我們的 finalizers 與垃圾回收器相連接,并且垃圾回收器回收以及清理數(shù)據(jù)的方式將會對 finalizers 的調(diào)用產(chǎn)生影響。

工作流

之前的例子可能讓我認(rèn)為 Go 僅在釋放我們所定義的 struct 的內(nèi)存之前調(diào)用 finalizers。

讓我們深入其中,看看在更多的 Allocation 中到底發(fā)生了些什么。

package main import (
 "fmt"  "math/rand"  "runtime"  "runtime/debug"  "strconv"  "time" ) type Foo struct {
 a int } func main() {
 debug.SetGCPercent(-1)

 var ms runtime.MemStats
 runtime.ReadMemStats(&ms)

 fmt.Printf("Allocation: %f Mb, Number of allocation: %d\n"float32(ms.HeapAlloc)/float32(1024*1204), ms.HeapObjects)

 for i := 0; i < 1000000; i++ {
  f := NewFoo(i)
  _ = fmt.Sprintf("%d", f.a)
 }

 runtime.ReadMemStats(&ms)
 fmt.Printf("Allocation: %f Mb, Number of allocation: %d\n"float32(ms.HeapAlloc)/float32(1024*1204), ms.HeapObjects)

 runtime.GC()
 time.Sleep(time.Second)

 runtime.ReadMemStats(&ms)
 fmt.Printf("Allocation: %f Mb, Number of allocation: %d\n"float32(ms.HeapAlloc)/float32(1024*1204), ms.HeapObjects)

 runtime.GC()
 time.Sleep(time.Second)
} //go:noinline func NewFoo(i int) *Foo {
 f := &Foo{a: rand.Intn(50)}
 runtime.SetFinalizer(f, func(f *Foo) {
  _ = fmt.Sprintf("foo " + strconv.Itoa(i) + " has been garbage collected")
 })

 return f
}

一百萬個 structs 和 finalizers 被創(chuàng)建出來,下面是輸出:

Allocation: 0.090862 Mb, Number of allocation: 137
Allocation: 31.107506 Mb, Number of allocation: 2390078
Allocation: 110.052666 Mb, Number of allocation: 4472742

讓我們再試一次,這次不用 finalizers:

Allocation: 0.090694 Mb, Number of allocation: 136
Allocation: 18.129814 Mb, Number of allocation: 1390078
Allocation: 0.094451 Mb, Number of allocation: 154

看起來沒有任何資源在內(nèi)存中被清理掉,即使垃圾回收器被觸發(fā),且 finalizers 也運(yùn)行。為了理解這一行為,讓我們回到那篇關(guān)于 runtime 的文檔:

當(dāng)垃圾回收器發(fā)現(xiàn)了一個已關(guān)聯(lián) finalizer 的無法訪問的塊,這說明了關(guān)聯(lián)操作與運(yùn)行 finalizer 是在一個單獨(dú)的 gorountine 下。這讓 obj 再次可訪問,不過現(xiàn)在沒有了一個關(guān)聯(lián)的 finalizer,假設(shè) SetFinalizer 沒有再次被調(diào)用,當(dāng)下次垃圾回收器看到這個 obj 時,它是不可被訪問的,并將回收它。

如我們所見,finalizers 首先會被移除,然后內(nèi)存將在下一次循環(huán)中被釋放,讓我們再次運(yùn)行第一個例子,并加上兩個強(qiáng)制的垃圾回收操作。

Allocation: 0.090862 Mb, Number of allocation: 137
Allocation: 31.107506 Mb, Number of allocation: 2390078
Allocation: 110.052666 Mb, Number of allocation: 4472742
Allocation: 0.099220 Mb, Number of allocation: 166

我們可以清楚地看到,第二次運(yùn)行將會清理數(shù)據(jù),finalizers 最終也對性能和內(nèi)存使用產(chǎn)生了輕微的作用。

性能表現(xiàn)

下文闡述了為何 finalizers 逐個運(yùn)行:

一個單獨(dú) goroutine 為了一個程序運(yùn)行了所有的 finalizers,然而,如果一個 finalizer 必須長時間運(yùn)行,則需要開啟一個新的 gorountine。

僅一個 goroutine 將會運(yùn)行 finalizers,并且任何超重任務(wù)都需要開啟一個新的 gorountine。當(dāng) finalizers 運(yùn)行時,垃圾回收器并沒有停止且并發(fā)運(yùn)行中。因此 finalizer 并不該影響你的應(yīng)用的性能表現(xiàn)。

同時,一旦 finalizer 不再被需要,Go 提供了一個方法來移除它。

 runtime.SetFinalizer(p, nil)

它允許我們根據(jù)使用情況動態(tài)地移除 finalizers。

應(yīng)用中的使用

內(nèi)部上,Go 在 net 以及 net/http 包中確保文件先前的打開與關(guān)閉準(zhǔn)確無誤,并且在 os 包中確保之前創(chuàng)建的進(jìn)程被正常地釋放。這里有一個來自 os 包的例子:

func newProcess(pid int, handle uintptr) *Process {
 p := &Process{Pid: pid, handle: handle}
 runtime.SetFinalizer(p, (*Process).Release)
 return p
}

當(dāng)這個進(jìn)程被釋放,finalizer 也會被移除。

func (p *Process) release() error {
 // NOOP for unix.  p.Pid = -1  // no need for a finalizer anymore  runtime.SetFinalizer(p, nil)
 return nil }

Go 同樣也在測試中使用 finalizers 確保在垃圾回收器中期望的動作被執(zhí)行,舉個例子,sync 包使用了 finalizers 測試在垃圾回收循環(huán)中 pool 是否被清空。

掃碼二維碼 獲取免費(fèi)視頻學(xué)習(xí)資料

Python編程學(xué)習(xí)

查 看2022高級編程視頻教程免費(fèi)獲取