Go垃圾回收系列之十一:finalizer的妙用
我只知道,如果在這里退縮了一步,那么過(guò)去那些重要的誓言、約定,就全部會(huì)消失不見(jiàn),而我再也無(wú)法回到這里了。
在面向?qū)ο蟮恼Z(yǔ)言中,析構(gòu)函數(shù)會(huì)在對(duì)象被銷毀的時(shí)候調(diào)用。finalizer是Go語(yǔ)言中的析構(gòu)函數(shù),可以由runtime.SetFinalizer函數(shù)將對(duì)象與finalizer函數(shù)綁在一起。當(dāng)對(duì)象不再被使用時(shí),可調(diào)用一個(gè)綁定的析構(gòu)函數(shù)。
一個(gè)極小的功能也可能有足夠驚艷的表現(xiàn)。在本文中,筆者將介紹finalizer應(yīng)用的場(chǎng)景和陷阱。我們知道Go的垃圾回收足夠的強(qiáng)大,但是卻沒(méi)有辦法管理所有的內(nèi)存,例如CGO的內(nèi)存、關(guān)閉操作系統(tǒng)資源描述符等。如果我們可以將finalizer函數(shù)與具體代表資源描述符的對(duì)象關(guān)聯(lián)起來(lái),那么就可以實(shí)現(xiàn)自動(dòng)關(guān)閉操作系統(tǒng)資源描述符的功能。實(shí)際上,GO語(yǔ)言就是這樣做的。在新建操作系統(tǒng)文件描述符時(shí),就完成了這一綁定。
func (fd *netFD) setAddr(laddr, raddr Addr) {
fd.laddr = laddr
fd.raddr = raddr
runtime.SetFinalizer(fd, (*netFD).Close)
}
func (fd *netFD) Close() error {
runtime.SetFinalizer(fd, nil)
return fd.pfd.Close()
}所以,我們其實(shí)不用close資源,也不會(huì)帶來(lái)資源泄露的困擾,但是手動(dòng)close 資源描述符仍然是有必要的。因?yàn)檫@會(huì)顯式的告訴我們資源已經(jīng)在此處不被使用了。
當(dāng)我們?cè)趯?shí)際中書(shū)寫(xiě)CGO程序時(shí),在Go語(yǔ)言調(diào)用C函數(shù)的時(shí)候,C函數(shù)分配的內(nèi)存并不受到Go垃圾回收的管理,這時(shí)我們常常借助defer 在函數(shù)調(diào)用結(jié)束時(shí)手動(dòng)釋放內(nèi)存。如下所示,在defer釋放C結(jié)構(gòu)體中的指針。
package main
// #include <stdio.h>
// typedef struct {
// char *msg;
// } myStruct;
// void myFunc(myStruct *strct) {
// printf("Hello %s!\n", strct->msg);
// }
import "C"
func main() {
msg := C.myStruct{C.CString("world")}
defer C.free(msg.msg)
C.myFunc(&msg)
}將其修改為finalizer的形式如下,這種方式將垃圾回收從函數(shù)結(jié)束后的defer 延遲到了垃圾回收階段,這延緩了垃圾回收對(duì)資源的釋放,在某些情況下實(shí)現(xiàn)了對(duì)內(nèi)存的單獨(dú)管理。需要注意的是,其中runtime.KeepAlive保證了finalizer的調(diào)用只能發(fā)生在該函數(shù)之后,這是為了避免一些嚴(yán)重的問(wèn)題,例如C.myFunc(msg.msg) 使用了msg.msg字段,這時(shí)由于msg已經(jīng)不再被引用,立即調(diào)用了finalizer,對(duì)msg.msg字段進(jìn)行了釋放,這時(shí)如果還在執(zhí)行C.myFunc,接觸msg.msg指針時(shí)就會(huì)報(bào)錯(cuò)。
import "C"
func main() {
msg := C.myStruct{C.CString("world")}
runtime.SetFinalizer(&msg, func(t *C.myStruct) {
C.free(unsafe.Pointer(t.msg))
})
C.myFunc(msg.msg)
runtime.KeepAlive(&msg)
}這種方式看上去很丑,對(duì)于一般的開(kāi)發(fā)者來(lái)講也比較的困擾,但是有用嗎?毫無(wú)疑問(wèn)是有用的,想象一下你希望開(kāi)發(fā)一個(gè)第三方庫(kù), 但是返回給用戶的對(duì)象引用了一個(gè)C分配的內(nèi)存對(duì)象,那么我們無(wú)法得知用戶什么時(shí)候會(huì)放棄使用該內(nèi)存,我們當(dāng)然不能夠在defer中釋放資源。但是我們又希望借助于自動(dòng)垃圾回收的功能,不用開(kāi)發(fā)者手動(dòng)的去調(diào)用free資源的函數(shù),這時(shí)這個(gè)功能就派上了用場(chǎng)。
在這里,要提到finalizer的一個(gè)陷阱。由于垃圾回收時(shí)調(diào)用finalizer很有可能是在另一個(gè)線程中執(zhí)行的,但有些資源可能不是線程安全的,例如在進(jìn)行GPU編程的時(shí)候。這時(shí)的一種解決辦法是,在finalizer函數(shù)中并不一定要執(zhí)行實(shí)際的資源釋放,可以只是將當(dāng)前指針用哈希表存儲(chǔ)起來(lái),并由綁定在統(tǒng)一線程上的協(xié)程定時(shí)釋放。
推薦閱讀
