Go: Finalizers 怎麼使用?
這篇文章基於 Go1.12 版本
Go runtime 提供了一種允許開發者將一個函數與一個變量綁定的方法 runtime.SetFinalizer
,被綁定的變量從它無法被訪問時就被垃圾回收器視爲待回收狀態。這個特性引起了高度的爭論,但本文並不打算參與其中,而是去闡述這個方法的具體實現。
無保障性
舉一個使用了 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
}
這段程序將會在這個循環中創建三個 struct 的的實例,並將每個實例都綁定一個 finalizer。之後垃圾回收器將會被調用, 並回收之前創建的實例。運行這個程序,將會給到我們如下輸出:
31
37
47
如我們所見,finalizers 並沒有被調用,runtime 的文檔解釋這一點:
在程序無法獲取到一個 obj 所指向的對象後的任意時刻,finalizer 被調度運行,且無法保證 finalizer 運行在程序退出之前。因此一般情況下,因此它們僅用於在長時間運行的程序上釋放一些與對象關聯的非內存資源。
在調用 finalizer 之前,runtime 不提供有關延遲的任何保證。讓我們試着去修改我們的程序,通過在調用垃圾回收器之後添加一個一秒的 sleep:
31
37
47
foo 1 has been garbage collected
foo 0 has been garbage collected
現在我們的 finalizer 已經被調用了,然而,它們其中一個消失了。我們的 finalizers 與垃圾回收器相連接,並且垃圾回收器回收以及清理數據的方式將會對 finalizers 的調用產生影響。
工作流
之前的例子可能讓我認爲 Go 僅在釋放我們所定義的 struct 的內存之前調用 finalizers。
讓我們深入其中,看看在更多的 Allocation 中到底發生了些什麼。
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 被創建出來,下面是輸出:
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
看起來沒有任何資源在內存中被清理掉,即使垃圾回收器被觸發,且 finalizers 也運行。爲了理解這一行爲,讓我們回到那篇關於 runtime 的文檔:
當垃圾回收器發現了一個已關聯 finalizer 的無法訪問的塊,這說明了關聯操作與運行 finalizer 是在一個單獨的 gorountine 下。這讓 obj 再次可訪問,不過現在沒有了一個關聯的 finalizer, 假設 SetFinalizer 沒有再次被調用,當下次垃圾回收器看到這個 obj 時,它是不可被訪問的,並將回收它。
如我們所見,finalizers 首先會被移除,然後內存將在下一次循環中被釋放,讓我們再次運行第一個例子,並加上兩個強制的垃圾回收操作。
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
我們可以清楚地看到,第二次運行將會清理數據,finalizers 最終也對性能和內存使用產生了輕微的作用。
性能表現
下文闡述了爲何 finalizers 逐個運行:
一個單獨 Goroutine 爲了一個程序運行了所有的 finalizers, 然而,如果一個 finalizer 必須長時間運行,則需要開啓一個新的 gorountine。
僅一個 Goroutine 將會運行 finalizers,並且任何超重任務都需要開啓一個新的 gorountine。當 finalizers 運行時,垃圾回收器並沒有停止且併發運行中。因此 finalizer 並不該影響你的應用的性能表現。
同時,一旦 finalizer 不再被需要,Go 提供了一個方法來移除它。
runtime.SetFinalizer(p, nil)
它允許我們根據使用情況動態地移除 finalizers。
應用中的使用
內部上,Go 在 net 以及 net/http 包中確保文件先前的打開與關閉準確無誤,並且在 os 包中確保之前創建的進程被正常地釋放。這裏有一個來自 os 包的例子:
func newProcess(pid int, handle uintptr) *Process {
p := &Process{Pid: pid, handle: handle}
runtime.SetFinalizer(p, (*Process).Release)
return p
}
當這個進程被釋放,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 確保在垃圾回收器中期望的動作被執行,舉個例子,sync 包使用了 finalizers 測試在垃圾回收循環中 pool 是否被清空。
via: https://medium.com/@blanchon.vincent/go-finalizers-786df8e17687
作者:Vincent Blanchon[1] 譯者:Maple24[2] 校對:polaris1119[3]
本文由 GCTT[4] 原創編譯,Go 中文網 [5] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。
參考資料
[1]
Vincent Blanchon: https://medium.com/@blanchon.vincent
[2]
Maple24: https://github.com/Maple24
[3]
polaris1119: https://github.com/polaris1119
[4]
GCTT: https://github.com/studygolang/GCTT
[5]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/H-VIhce4N3aH5HIhY-5UlQ