Lua 技術ノート 6

弱参照:Luaにおける実装と利用

by 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()`のロックフラグによって直接サポートされます。値が0の場合、オブジェクトはガベージコレクションされることを許可します。

私たちの`weakref`関数は`lua_ref()`を呼び出して、結果の参照IDを保持するオブジェクトを返す必要があります。参照オブジェクトの関数呼び出しタグメソッドで実装された逆参照は、単に`lua_getref()`を呼び出します。最後に、参照オブジェクト自体が収集されたときに参照を解放する必要があるため、ガベージコレクション(gc)タグメソッドを使用して`lua_unref()`を呼び出します。

ユーザーデータ型は、gcイベントを提供する唯一の型であるため、参照オブジェクトに最適な選択です。さらに、必要なのは整数1つだけなので、状態をユーザーデータポインタ自体に格納することで、動的なメモリ割り当てを排除できます。

この実装のソースコードは、公式Lua 4.0配布版へのパッチとして提供されており、こちらで入手できます。パッチユーティリティを使用して次のように適用します。

    cd <lua distrubution directory>
    patch -p1 < weakrefs.patch
このパッチには、拡張機能の簡単な例を示すテストディレクトリ「weakref.lua」への新しい追加が含まれています。

この実装をLuaの「baselib」標準ライブラリに追加することを提案します。その理由は、弱参照は一般的に有用であり、実装はシンプルでC APIによって既にサポートされていること、そして新しいLua関数は1つだけなので、その目的のために別のライブラリを作成するのは過剰であるためです。

安全なオブジェクトデストラクタ

ガベージコレクションのある言語では、デストラクタで最も一般的なニーズである(破壊されるオブジェクトが所有する)他のオブジェクトの解放は不要になります。その結果、Luaプログラマは(テーブル)オブジェクトのgcイベントの欠如をほとんど気にすることはありません。このようなイベントがサポートされていない理由は、主にガベージコレクタをシンプルに保つためです。gcイベントが許可されている場合、コレクタは、デストラクタが収集されるオブジェクトへの新しい参照を作成する場合に対処する必要があります。

しかし、ファイルハンドル、グラフィックバッファなど、自動的に解放されないシステムリソースをオブジェクトが所有する場合があります。やや面倒な解決策は、gcイベントを持つユーザーデータ型を使用してCからそのようなオブジェクトを実装することです。弱参照はこれに対するエレガントな代替手段を提供し、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ページを動的に生成するWebサーバーを考えてみましょう。この種のアプリケーションでは、パフォーマンスを向上させるために、生成されたページをメモリにキャッシュすることが一般的です。しかし、ページオブジェクトをテーブルに単純に格納して実装した場合、それらは決して収集されず、メモリ使用量は制御不能に増加します。

1つの解決策は、最近アクセスされた上位*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氏に、C APIが弱参照をサポートしているという自明の理を親切に指摘してくれたRoberto Ierusalimschy氏に、そしてここで提示したソースコードを共有することを許可してくれたNanaOn-Sha株式会社とソニー・コンピュータエンタテインメント株式会社に感謝いたします。


最終更新日:2004年6月16日(水) 10:43:27 BRT