Lua 技術備註 6

弱參照:在 Lua 中的實作和使用

作者 John Belmonte

概述

在使用垃圾回收的電腦語言中,例如 Lua,如果參照物件不會阻止物件被回收,則稱此參照為弱參照。弱參照可用於判斷物件何時已被回收,以及在不阻礙物件被回收的情況下快取物件。

雖然 Lua C API 中有提供弱參照,但 Lua 語言本身並沒有提供標準支援。此備註提出 Lua 中弱參照的介面,說明實作方式,並提供一些實際使用範例:表格物件的安全解構函式和物件快取。

介面

以下是建議介面的概要
    -- 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 發行版的修補程式,可在此處取得 here。使用修補程式工具套用,如下所示

    cd <lua distrubution directory>
    patch -p1 < weakrefs.patch
此修補程式包括測試目錄「weakref.lua」的新增內容,其中顯示此擴充功能的一個簡單範例。

建議將此實作新增至 Lua 的「baselib」標準函式庫,原因有數:弱參照具有普遍的實用性;實作簡單,且已由 C API 支援;而且由於只需要一個新的 Lua 函式,因此為其目的建立獨立的函式庫將會過於繁瑣。

安全的物件解構函式

在具有垃圾回收功能的語言中,最常見的解構函式需求(釋放被解構物件所擁有的其他物件)已不復存在。因此,Lua 程式設計師很少會錯過(表格)物件的 gc 事件。不支援此類事件的主要原因是為了讓垃圾回收器保持簡單。如果允許 gc 事件,則回收器必須處理解構函式對正在收集的物件建立新參照的情況。

然而,在某些情況下,物件會擁有不會自動釋放的系統資源,例如檔案句柄、圖形緩衝區等。一個有點繁瑣的解決方案是使用 userdata 類型以 C 實作此類物件,而此類型確實有 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

物件快取

考慮一個從資料庫(例如郵件清單或程式原始碼)動態產生網頁的網頁伺服器。在這種應用程式中,快取已產生的網頁至記憶體以提升效能是很常見的。然而,如果快取是透過將網頁物件單純儲存在表格中來實作,它們將永遠不會被收集,而記憶體使用量將會持續不受控地增加。

一種補救方法是僅快取最近存取的 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 巴西時間