Lua 技术说明 6

弱引用:在 Lua 中的实现和使用

作者 John Belmonte

概述

在使用垃圾回收的计算机语言(如 Lua)中,如果引用不会阻止对象被回收,则称该引用为弱引用。弱引用可用于确定对象何时被回收,以及在不阻止对象被回收的情况下缓存对象。

虽然 Lua C API 中提供了弱引用,但 Lua 语言本身中没有提供标准支持。本说明针对 Lua 中的弱引用提出一个接口,描述一个实现,并提供一些实际使用示例:表对象的 safe 析构函数事件和对象缓存。

接口

以下是所提议接口的概要
    -- creation
    ref = weakref(obj)
    -- dereference
    obj = ref()
也就是说,使用名为“weakref”的新全局函数来创建对对象的弱引用。可以使用函数调用运算符取消引用弱引用。返回 nil 的取消引用表示对象已被垃圾回收。由于 nil 具有此特殊含义,因此不允许对 nil 对象本身进行弱引用。

实现

Lua C API 提供了一个用于引用 Lua 对象的接口。lua_ref() 的锁定标志直接支持弱引用:零值允许对象被垃圾回收。

我们的 weakref 函数需要调用 lua_ref() 并返回一个保存结果引用 ID 的对象。取消引用(使用引用对象上的函数调用标记方法实现)只需调用 lua_getref()。最后,当引用对象本身被回收时,有必要释放引用,因此使用垃圾回收 (gc) 标记方法来调用 lua_unref()。

用户数据类型是引用对象的自然选择,因为它是唯一提供 gc 事件的类型。此外,由于只需要一个整数,因此可以将状态存储为用户数据指针本身,从而消除了动态内存分配。

此实现的源代码作为官方 Lua 4.0 发行版的补丁提供,可在此处获取。使用补丁实用程序按如下方式应用

    cd <lua distrubution directory>
    patch -p1 < weakrefs.patch
该补丁包括测试目录“weakref.lua”的新增内容,其中显示了该扩展的一个简单示例。

建议将此实现添加到 Lua 的“baselib”标准库中,原因有以下几点:弱引用具有普遍的实用性;该实现简单,并且已得到 C API 的支持;并且由于只需要一个新的 Lua 函数,因此为其目的创建单独的库将是多余的。

安全对象析构函数

在具有垃圾回收的语言中,对析构函数最常见的需求——释放被析构对象所拥有的其他对象——已被消除。因此,Lua 程序员很少会错过(表)对象的 gc 事件。不支持此类事件的主要原因是为了保持垃圾回收器的简单性。如果允许 gc 事件,则回收器必须处理析构函数对正在回收的对象进行全新引用的情况。

但是,在某些情况下,对象会拥有未自动释放的某些系统资源,例如文件句柄、图形缓冲区等。一个有点繁琐的解决方案是用 C 从 userdata 类型实现此类对象,该类型确实具有 gc 事件。弱引用为此提供了一种优雅的替代方案,允许从 Lua 的舒适环境中对表对象进行安全的垃圾回收事件。

该实现使用包含弱引用/析构函数对的表。当引用的对象被回收时,将调用相应的析构函数。这些析构函数是安全的,因为它们无法访问正在销毁的对象。析构函数所需的任何信息(例如资源句柄)都必须独立于对象访问。这对于 Lua 来说是一项相当轻松的工作,这要归功于一流函数对象。

需要一个小型接口来管理表,该接口包含一个将析构函数绑定到对象的功能和一个检查已回收对象的功能。以下是 Lua 中的实现

    ------------------------------------------
    -- Destructor manager

    local destructor_table = { }

    function RegisterDestructor(obj, destructor)
        %destructor_table[weakref(obj)] = destructor
    end

    function CheckDestructors()
        local delete_list = { }
        for objref, destructor in %destructor_table do
            if not objref() then
                destructor()
                tinsert(delete_list, objref)
            end
        end
        for i = 1, getn(delete_list) do
            %destructor_table[delete_list[i]] = nil
        end
    end
与其在某个时间间隔手动调用 CheckDestructors(),自然而然的想法是将其链接到 Lua 的垃圾回收周期。Lua 虚拟机通过在周期结束时调用 nil 类型的 gc 标记方法来支持此操作。

作为安全析构函数使用的示例,考虑一个用于将程序消息记录到文件中的对象。当对象被垃圾回收时,我们希望日志文件被关闭。(此示例很琐碎,因为文件句柄在程序终止时关闭。但是,该方法很容易应用于其他类型的资源。)

    ------------------------------------------
    -- example object using safe destructor

    function make_logobj(filename)
        local id = openfile(filename, "w")
        assert(id)

        local obj =
        {
            file = id,

            write = function(self, message)
                write(self.file, message)
            end,
        }

        local destructor = function()
            closefile(%id)
        end

        RegisterDestructor(obj, destructor)
        return obj
    end

对象缓存

考虑一个从数据库(例如邮件列表或程序源代码)动态生成网页的 Web 服务器。在这种类型的应用程序中,通常会将生成的页面缓存在内存中以提高性能。但是,如果缓存通过简单地将页面对象存储在表中来实现,那么它们将永远不会被回收,并且内存使用量将不受控制地增长。

一种补救措施是仅缓存最近访问的n页,但由于未考虑数据大小,这不会充分利用可用内存。一种改进方法是缓存最近访问的x千字节生成数据。除了增加程序复杂性之外,这里出现的问题是找到一个合适的x值。这类似于垃圾收集器面临的问题:应该多久进行一次循环以及在使用多少内存后进行循环?

通过对缓存使用弱引用,在将内存使用问题留给垃圾收集器的情况下,程序复杂性保持较低。缓存表不是存储生成的页面对象,而是包含对这些对象的弱引用。当垃圾回收周期发生时,当前未使用的页面对象将被回收。

这里有一个实现,假设一个函数 GeneratePage() 给定其“名称”来生成一个页面对象。需要 CleanCache() 函数来删除已收集对象的表项,该函数又应该链接到 Lua 的 gc 周期。

    ------------------------------------------
    -- Page cache

    local cache_table = { }

    function GetPage(name)
        local ref = %cache_table[name]
        local obj = ref and ref()
        if not obj then
            obj = GeneratePage(name)
            %cache_table[name] = weakref(obj)
        end
        return obj
    end

    function CleanCache()
        local delete_list = { }
        for name, ref in %cache_table do
            if not ref() then
                tinsert(delete_list, name)
            end
        end
        for i = 1, getn(delete_list) do
            %cache_table[delete_list[i]] = nil
        end
    end

致谢

作者要感谢 Anthony Carrico 关于弱引用和垃圾回收的讨论,感谢 Roberto Ierusalimschy 善意地指出显而易见的事实(C API 支持弱引用),还要感谢 NanaOn-Sha, Co. Ltd. 和 Sony Computer Entertainment, Inc. 允许共享此处提供的源代码。


最后更新:2004 年 6 月 16 日星期三上午 10:43:27 巴西利亚时间