关于Go的终结器你要慎重

205 阅读4分钟

1.终结器概述

我们能不用就别用,但需要了解,因为下面有很多需要注意的事情.

runtime.SetFinalizer(objPointer, func(p *objType){

})

2.终结器的时机和弊端

2.1 时机不可控,且会暂时复活对象

SetFinalizer 将与 obj 关联的终结器设置为提供的 终结函数。 当垃圾收集器发现无法访问的块时 使用关联的终结器,它会清除关联并运行 finalizer(obj) 在一个单独的 goroutine 中。 这使得 obj 再次可达, 但现在没有关联的终结器。 假设 SetFinalizer 不会被再次调用,下次垃圾收集器看到 那个 obj 是不可访问的,它会释放 obj。

2.2 SetFinalizer(obj, nil) 清除与 obj 关联的任何终结器

参数 obj 必须是指向通过调用分配的对象的指针 new,通过获取复合文字的地址,或者通过获取 局部变量的地址。

参数终结器必须是一个接受单个参数的函数 obj 的类型可以赋值给哪个,可以任意忽略 return 值。 如果其中任何一个不为真,SetFinalizer 可能会中止 程序。

2.3 依赖问题

终结器按依赖顺序运行:如果 A 指向 B,则两者都有 终结器,否则它们是不可访问的,只有终结器 对于 A 运行; 一旦 A 被释放,B 的终结器就可以运行。 如果一个循环结构包含一个带有终结器的块,那么 循环不保证被垃圾收集和终结器 不保证运行,因为没有命令 尊重依赖关系。

2.4 不能保证终结器会在程序退出之前运行

终结器计划在某个任意时间运行 程序无法再到达 obj 指向的对象。 不能保证终结器会在程序退出之前运行, 所以通常它们只对释放非内存资源有用 在长时间运行的程序中与对象关联。 例如,一个 os.File 对象可以使用终结器来关闭 程序丢弃时关联的操作系统文件描述符 一个没有调用 Close 的 os.File,但这是一个错误 依靠终结器来刷新内存中的 I/O 缓冲区,例如 bufio.Writer,因为缓冲区不会在程序退出时被刷新。

如果 *obj 的大小为 零字节,因为它可能与其他零大小共享相同的地址 内存中的对象。 请参阅 https:go.dev/ref/spec#Size_and_alignment_guarantees。

不能保证终结器会为分配的对象运行 在包级变量的初始值设定项中。 这些对象可能是 链接器分配的,而不是堆分配的。

2.5 终结器可能会在未来任意执行

请注意,因为终结器可能会在未来任意执行 一个对象不再被引用后,允许运行时执行 一种节省空间的优化,将对象一起批量处理 分配槽。 此类中未引用对象的终结器 如果分配总是与 a 存在于同一批次中,则分配可能永远不会运行 引用的对象。 通常,这种批处理只发生在微小的 (大约 16 个字节或更少)和无指针对象。

终结器可能会在对象变得不可访问时立即运行。 为了正确使用终结器,程序必须确保 在不再需要该对象之前,该对象是可访问的。 存储在全局变量中的对象,或者可以通过跟踪找到的对象 来自全局变量的指针是可达的。 对于其他对象, 将对象传递给 KeepAlive 函数的调用以标记 函数中对象必须可达的最后一点。

例如,如果 p 指向一个结构,例如 os.File,它包含 一个文件描述符 d,p 有一个关闭该文件的终结器 描述符,如果函数中最后一次使用 p 是调用 syscall.Write(p.d, buf, size),那么一旦 p 可能无法访问 程序进入syscall.Write。 终结器可能会在那一刻运行, 关闭 p.d,导致 syscall.Write 失败,因为它正在写入 一个关闭的文件描述符(或者,更糟的是,一个完全不同的 由不同的 goroutine 打开的文件描述符)。 为了避免这个问题, 在调用 syscall.Write 之后调用 KeepAlive(p)。

单个 goroutine 按顺序运行程序的所有终结器。 如果终结器必须运行很长时间,它应该通过启动来实现 一个新的协程。

2.6 通常终结器应该使用互斥量或其他同步机制

在 Go 内存模型的术语中,一个调用 SetFinalizer(x, f) 在终结调用 f(x) 之前“同步”。 但是,不能保证 KeepAlive(x) 或 x 的任何其他使用 “在”f(x) 之前“同步”,所以通常终结器应该使用互斥量 或其他同步机制,如果它需要访问 x 中的可变状态。 例如,考虑一个检查 x 中可变字段的终结器 即在主程序中不时修改x之前 变得不可访问并调用终结器。 主程序中的修改和终结器中的检查 需要使用适当的同步,例如互斥或原子更新, 避免读写竞争。