Go: 延長(zhǎng)變量的生命周期

本文基于 Go 1.13。
在 Go 中,我們不需要自己管理內(nèi)存分配和釋放。然而,有些時(shí)候我們需要對(duì)程序進(jìn)行更細(xì)粒度的控制。Go 運(yùn)行時(shí)提供了很多種控制運(yùn)行時(shí)狀態(tài)及其與內(nèi)存管理器之間相互影響的方式。本文中,我們來(lái)審查讓變量不被 GC 回收的能力。
場(chǎng)景
我們來(lái)看一個(gè)基于 Go 官方文檔[1] 的例子:
type File struct { d int }
func main() {
p := openFile("t.txt")
content := readFile(p.d)
println("Here is the content: "+content)
}
func openFile(path string) *File {
d, err := syscall.Open(path, syscall.O_RDONLY, 0)
if err != nil {
panic(err)
}
p := &Filego7utgvlrp
runtime.SetFinalizer(p, func(p *File) {
syscall.Close(p.d)
})
return p
}
func readFile(descriptor int) string {
doSomeAllocation()
var buf [1000]byte
_, err := syscall.Read(descriptor, buf[:])
if err != nil {
panic(err)
}
return string(buf[:])
}
func doSomeAllocation() {
var a *int
// memory increase to force the GC
for i:= 0; i < 10000000; i++ {
i := 1
a = &i
}
_ = a
}
源碼地址[2]
這個(gè)程序中一個(gè)函數(shù)打開(kāi)文件,另一個(gè)函數(shù)讀取文件。代表文件的結(jié)構(gòu)體注冊(cè)了一個(gè) finalizer,在 gc 釋放結(jié)構(gòu)體時(shí)自動(dòng)關(guān)閉文件。運(yùn)行這個(gè)程序,會(huì)出現(xiàn) panic:
panic: bad file descriptor
goroutine 1 [running]:
main.readFile(0x3, 0x5, 0xc000078008)
main.go:42 +0x103
main.main()
main.go:14 +0x4b
exit status 2
下面是流程圖:
打開(kāi)文件,返回一個(gè)文件描述符 這個(gè)文件描述符被傳遞給讀取文件的函數(shù) 這個(gè)函數(shù)首先做一些繁重的工作:

allocate 函數(shù)觸發(fā) gc:

因?yàn)槲募枋龇莻€(gè)整型,并以副本傳遞,所以打開(kāi)文件的函數(shù)返回的結(jié)構(gòu)體 *File* 不再被引用。Gc 把它標(biāo)記為可以被回收的。之后觸發(fā)這個(gè)變量注冊(cè)的 finalizer,關(guān)閉文件。
然后,主協(xié)程讀取文件:

因?yàn)槲募呀?jīng)被 finalizer 關(guān)閉,所以會(huì)出現(xiàn) panic。
讓變量不被回收
runtime 包暴露了一個(gè)方法,用來(lái)在 Go 程序中避免出現(xiàn)這種情況,并顯式地聲明了讓變量不被回收。在運(yùn)行到這個(gè)調(diào)用這個(gè)方法的地方之前,gc 不會(huì)清除指定的變量。下面是加了對(duì)這個(gè)方法的調(diào)用的新代碼:

在運(yùn)行到 keepAlive 方法之前,gc 不能回收變量 p。如果你再運(yùn)行一次程序,它會(huì)正常讀取文件并成功終止。
追本逐源
keepAlive 方法本身沒(méi)有做什么:

運(yùn)行時(shí),Go 編譯器會(huì)以很多種方式優(yōu)化代碼:函數(shù)內(nèi)聯(lián),死碼消除,等等。這個(gè)函數(shù)不會(huì)被內(nèi)聯(lián),Go 編譯器可以輕易地探測(cè)到哪里調(diào)用了 keepAlive。編譯器很容易追蹤到調(diào)用它的地方,它會(huì)發(fā)出一個(gè)特殊的 SSA 指令,以此來(lái)確保它不會(huì)被 gc 回收。

在生成的 SSA 代碼中也可以看到這個(gè) SSA 指令:

在我的文章 Go 編譯器概述[3] 中你可以看到更多關(guān)于 Go 編譯器和 SSA 的信息。
via: https://medium.com/a-journey-with-go/go-keeping-a-variable-alive-c28e3633673a
作者:Vincent Blanchon[4]譯者:lxbwolf[5]校對(duì):polaris1119[6]
本文由 GCTT[7] 原創(chuàng)編譯,Go 中文網(wǎng)[8] 榮譽(yù)推出
參考資料
Go 官方文檔: https://golang.org/pkg/runtime/#KeepAlive
[2]源碼地址: https://gist.githubusercontent.com/blanchonvincent/a247b6c2af559b62f93377b5d7581b7f/raw/6488ec2a36c28c46f942b7ac8f24af4e75c19a2f/main.go
[3]Go 編譯器概述: https://medium.com/a-journey-with-go/go-overview-of-the-compiler-4e5a153ca889
[4]Vincent Blanchon: https://medium.com/@blanchon.vincent
[5]lxbwolf: https://github.com/lxbwolf
[6]polaris1119: https://github.com/polaris1119
[7]GCTT: https://github.com/studygolang/GCTT
[8]Go 中文網(wǎng): https://studygolang.com/
推薦閱讀
