Go垃圾回收系列之十一:finalizer的妙用
共 2242字,需浏览 5分钟
·
2021-04-06 19:39
我只知道,如果在这里退缩了一步,那么过去那些重要的誓言、约定,就全部会消失不见,而我再也无法回到这里了。
在面向对象的语言中,析构函数会在对象被销毁的时候调用。finalizer是Go语言中的析构函数,可以由runtime.SetFinalizer函数将对象与finalizer函数绑在一起。当对象不再被使用时,可调用一个绑定的析构函数。
一个极小的功能也可能有足够惊艳的表现。在本文中,笔者将介绍finalizer应用的场景和陷阱。我们知道Go的垃圾回收足够的强大,但是却没有办法管理所有的内存,例如CGO的内存、关闭操作系统资源描述符等。如果我们可以将finalizer函数与具体代表资源描述符的对象关联起来,那么就可以实现自动关闭操作系统资源描述符的功能。实际上,GO语言就是这样做的。在新建操作系统文件描述符时,就完成了这一绑定。
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()
}
所以,我们其实不用close资源,也不会带来资源泄露的困扰,但是手动close 资源描述符仍然是有必要的。因为这会显式的告诉我们资源已经在此处不被使用了。
当我们在实际中书写CGO程序时,在Go语言调用C函数的时候,C函数分配的内存并不受到Go垃圾回收的管理,这时我们常常借助defer 在函数调用结束时手动释放内存。如下所示,在defer释放C结构体中的指针。
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的形式如下,这种方式将垃圾回收从函数结束后的defer 延迟到了垃圾回收阶段,这延缓了垃圾回收对资源的释放,在某些情况下实现了对内存的单独管理。需要注意的是,其中runtime.KeepAlive保证了finalizer的调用只能发生在该函数之后,这是为了避免一些严重的问题,例如C.myFunc(msg.msg) 使用了msg.msg字段,这时由于msg已经不再被引用,立即调用了finalizer,对msg.msg字段进行了释放,这时如果还在执行C.myFunc,接触msg.msg指针时就会报错。
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)
}
这种方式看上去很丑,对于一般的开发者来讲也比较的困扰,但是有用吗?毫无疑问是有用的,想象一下你希望开发一个第三方库, 但是返回给用户的对象引用了一个C分配的内存对象,那么我们无法得知用户什么时候会放弃使用该内存,我们当然不能够在defer中释放资源。但是我们又希望借助于自动垃圾回收的功能,不用开发者手动的去调用free资源的函数,这时这个功能就派上了用场。
在这里,要提到finalizer的一个陷阱。由于垃圾回收时调用finalizer很有可能是在另一个线程中执行的,但有些资源可能不是线程安全的,例如在进行GPU编程的时候。这时的一种解决办法是,在finalizer函数中并不一定要执行实际的资源释放,可以只是将当前指针用哈希表存储起来,并由绑定在统一线程上的协程定时释放。
推荐阅读