首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Go 1.24: runtime.AddCleanup, 改进 runtime.SetFinalizer 的一些问题

Go 1.24: runtime.AddCleanup, 改进 runtime.SetFinalizer 的一些问题

作者头像
萝卜要努力
发布2025-03-07 16:02:22
发布2025-03-07 16:02:22
3420
举报
文章被收录于专栏:萝卜要加油萝卜要加油

以前,我写过一篇文章介绍runtime.SetFinalizer 这个函数,用于在对象被清理的时候调用,但是这个函数有一些问题,导致它的使用频率比较低。

  1. 在 Go 中使用 SetFinalizer 方法时,必须确保传递给它的对象引用指向分配内存的起始位置(即分配内存块的第一个字节)。这要求程序员理解“分配”(allocation)的概念,而这个概念通常在 Go 语言的抽象层级中并未明确暴露。
  2. 对象只能定义一个SetFinalizer
  3. 带有SetFinalizer的对象如果涉及到任何引用循环,将无法被释放,SetFinalizer也不会运行。
  4. 带有SetFinalizer的对象至少需要两个 GC 循环才能被释放。 https://github.com/golang/go/issues/67535

对于第一条,我们可以用一个例子说明

代码语言:javascript
复制
package main

import (
"fmt"
"runtime"
)

type MyStruct struct {
    Field int
}

funcmain() {
    obj := &MyStruct{Field: 42}
    runtime.SetFinalizer(obj, func(m *MyStruct) {
        fmt.Println("Finalizer called for:", m)
    })

// 错误示例:传递字段引用而非对象本身
// runtime.SetFinalizer(&obj.Field, func(m *int) {
// fmt.Println("Finalizer called for:", *m)
// })
}

基于上面的原因,在Golang 1.24 中添加了一个新的函数runtime.AddCleanUp 来替换runtime.SetFinalizer

注意:runtime.AddCleanupruntime.SetFinalizer 都不保证 清理函数一定会被执行。

AddCleanup的设计目标是解决runtime.SetFinalizer的诸多问题,特别是避免对象复活,从而允许对象的及时清理,并支持对象的循环清理。 AddCleanup函数的原型如下:

代码语言:javascript
复制
funcAddCleanup[T,Sany](ptr *T, cleanupfunc(S), arg S) Cleanup

基于此,可以写一个RAII(Resource Acquisition Is Initialization)的 Demo 。在go weak 的文章中,我们实现了一个固定长度的cache,改一下代码,添加一个newElemWeak 方法,当最旧的elem 被逐出的时候,会自动调用deletekeycache 中删除。不需要我们手动管理。

代码语言:javascript
复制
func(c *WeakCache) newElemWeak(elem *list.Element) weak.Pointer[list.Element] {
    elemWeak := weak.Make(elem)
    runtime.AddCleanup(elem, func(namestring) {
        delete(c.cache, name)
    }, elem.Value.(*CacheItem).key)
    return elemWeak
}

需要注意的几个问题

AddCleanupptr的约束很少,支持为同一个指针附加多个清理函数。不过,如果ptr可以从cleanuparg中可达,ptr将永远不会被回收(memory leak),清理函数也永远不会运行,目前来看这种情况也不会panic。 或许以后会用GODEBUG=gccheckmark=1 这种模式来检测? 比如

代码语言:javascript
复制
funcNewFileResource(filenamestring) (*os.File, error) {
    file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
returnnil, err
    }

    runtime.AddCleanup(file, func(f *os.File) {
        fmt.Println("close f")
        _ = f.Close()
    }, file)

return file, nil
}

fmt.Println("close f") 不会被打印,file 也不会被关闭。

Try It 当给一个ptr绑定多个cleanup 的时候,因为cleanup 是在一个独立的goroutine 中运行,所以它的运行顺序可能是不固定的。

特别是,如果几个对象相互指向并且同时变成 unreachable,它们的清理函数都可以运行,并且可以以任何顺序运行。即使对象形成一个循环也是如此(runtime.SetFinalizer 在这种情况下会产生内存泄露)。 例如

代码语言:javascript
复制
funcmain() {
    x := MyStruct{Name: "X"}
    y := MyStruct{Name: "Y"}

    x.Other = &y
    y.Other = &x
//runtime.SetFinalizer(&x, func(x *MyStruct) {
// fmt.Printf("Finalizer for %s is called\n", x.Name)
//})
//runtime.SetFinalizer(&y, func(y *MyStruct) {
// fmt.Printf("Finalizer for %s is called\n", y.Name)
//})
    xName := x.Name
    runtime.AddCleanup(&x, func(namestring) {
        fmt.Println("Cleanup for", x)
    }, xName)
    yName := y.Name
    runtime.AddCleanup(&y, func(namestring) {
        fmt.Println("Cleanup for", x)
    }, yName)
    time.Sleep(time.Millisecond)
    runtime.GC()
    time.Sleep(time.Millisecond)
    runtime.GC()
}

运行结果

代码语言:javascript
复制
➜  AddCleanUp git:(main) ✗ gotip run main.go
Cleanup for Y
Cleanup for X

SetFinalizer 的会阻止 GC,但是AddCleanup 能够正常执行

参考资料

  1. https://github.com/golang/go/issues/67535
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 萝卜要加油 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 需要注意的几个问题
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档