1

私は、オブザーバー パターンを多用してすべてのデータ オブジェクトの状態が最新であることを確認する Java プロジェクトに取り組んでいます。私はこの混乱を維持することにうんざりしており、貴重なデータ オブジェクトからオブザーバー パターンの恐怖を切り離すソリューションを実装しようとしています。

このプロジェクトの詳細を抽象化して、解決しようとしている問題は次のとおりであると言うことができました。

式を表す一連のオブジェクトが存在し、それぞれが他の式の値に依存する可能性があります。

次の 2 つの操作が必要です。

eval(): 指定された式の値を取得します

この操作は、すべての式の依存関係が今再評価された場合に返される式の最新の値を返す必要があります。ただし、2 回目の操作でキャッシュが無効にされない限り、式を複数回評価する必要はありません。

update(): 指定された式を変更します

この操作は、式のキャッシュと、それに直接または推移的に依存する現在キャッシュされているすべての式のキャッシュを無効にします。

さらに、式の有効期間を管理するためのメモリ リークのない便利な方法が必要です。

擬似コードでの望ましい使用例:

Expression a = variable(1);
Expression b = variable(3);
Expression s = sum(a,b);
assert(4 == eval(s));    // causes evaluation of expressions a, b and s
assert(4 == eval(s));    // does not cause any evaluations,
                         //     the result should be taken from cache
setValue(a,2);           // contains update() internally, 
                         //     invalidating caches for a and s
assert(5 == eval(s));    // causes evaluation of a and s

OK、機能的な部分は終わりました。ここからメモリ管理の部分に進みます。

開発者が式グラフを管理する簡単な方法が必要です。理想的には、割り当ては で行われるべきでありnew Sum(a,b)、開発者はキャッシュについてあまり知識がなくても好きなように式のインスタンスを自由に渡すことができるべきであり、割り当て解除は開発者側の努力なしで自動的に行われるべきです。

また、メモリ リークがあってはなりません。つまり、式の割り当てが解除されるとき、それに関連付けられたメモリには何も残っていてはなりません。たとえば、オブザーバー パターンを無効化に使用する場合、その式をすべてのオブザーバー リストから削除する必要があります。

質問は:

これをお気に入りの言語で実装するには、どのようなアプローチをとりますか?

ガベージコレクションされていない関数型言語も大歓迎です。特に関数型言語は、純粋な関数型でこの問題にアプローチする方法がまったくわからないためです。

私の観点からの最善の解決策は、開発者のエラーの可能性が最も少ないものです。

現在の実装の詳細を意図的に公開していません。実装に根本的な欠陥を見つけたと思いますが、それを回避する方法が見当たらないからです。後で投稿しますが。

4

2 に答える 2

1

誰かが興味を持っている場合 (おそらく誰も興味がないでしょう)、私はグローバル キャッシュのアイデアを放棄し、Expressionセルフ キャッシュを作成することで問題を解決する必要がありました。

という基本クラスにすべてのロジックを実装しましたExpressionBase

ソリューションには以下が含まれます。

  • 式には、その依存関係への弱参照のリストが含まれており、変更時に通知されます。この方法では、メモリ リークが発生せず、サブスクライブを解除する必要もありません。
  • 式の評価中に、以前の回答で説明したのと同様の方法で依存関係を自動的に検出し、それらをサブスクライブします。
  • 依存関係のリストは、中間式のガベージ コレクションが早すぎるのを防ぐために保持されます (SumProxyExpression前の回答の場合)。このようにして、すべての弱参照には逆の強参照があり、これらの連鎖がどこにも通じない限り、弱参照の連鎖が GC によって壊れることはありません。
于 2010-12-25T05:10:09.507 に答える
0

OK、ここで Java 言語を使用して問題へのアプローチを説明しようと思います。

SumExpression (他の 2 つの式の結果を加算するために使用される式) の例ですべてを説明します。

ユーザーコード

私は、最も単純なアプローチである Observer パターンから始めました。すべての式は、キャッシュの無効化のために依存関係をリッスンします。この方法で実装された SumExpression のバージョンを次に示します。

public class SumExpression implements Expression<Integer> {
    private final Expression<Integer> a;
    private final Expression<Integer> b;

    Integer value;
    private Listener invalidator = new Listener() {
        @Override
        public void changed() {
            invalidate();
        }
    };

    public SumExpression(SimpleVariable<Integer> a, SimpleVariable<Integer> b) {
        this.a = a;
        this.b = b;
        a.listeners().addListener(invalidator);// don't forget to call it!
        b.listeners().addListener(invalidator);
    }

    public Integer getValue()
    {
        validate();
        return value;
    }

    private void validate() {
        if(value == null)
            value = evaluate;
    }

    private void evaluate() {
        value = null;
    }

    public void dispose() { // USER, DON'T FORGET TO CALL IT!!!
        a.removeListener(invalidator);
        b.removeListener(invalidator);
    }

    ListenerCollection listeners = new ListenerCollection();

    @Override
    public void addListener(Listener l) {
        listeners.addListener(l);
    }

    @Override
    public void removeListener(Listener l) {
        listeners.removeListener(l);
    }
}

ただし、うまくいかない可能性がある場所はたくさんあります。2 つの数値の足し算のような単純なものは、はるかに単純なはずです。そのため、次の方法でキャッシュからロジックを切り離しました。

public class SumExpression implements Expression<Integer> {
    private final Expression<Integer> a;
    private final Expression<Integer> b;

    public SumExpression(Expression<Integer> a, Expression<Integer> b)
    {
        this.a = a;
        this.b = b;
    }

    public Integer evaluate(EvaluationContext context)
    {
        return context.getValue(a)+context.getValue(b);
    }
}

はるかに簡単ですね。ここでEvaluationContextの役割は 2 つあることに注意してください。キャッシュから値を取得し、SumExpressionと 式aとの間の依存関係のリストを収集しbます。

コアコード

次に、EvaluationContextキャッシュされたデータを と同様の構造で格納するグローバル キャッシング クラスとWeakHashMap<Expression, Object>、タイプが のノードを持つ DAG のディペンデンシー グラフ データを提供しWeakReference<Expression>ます。

これが私のevalupdateの実装です:

public <T1> T1 eval(final Expression<T1> expression)
{
    Weak weak = weaken(expression);
    T1 result = (T1) cache.get(weak);
    if(result == null) {
        result = expression.evaluate(new EvaluationContext()
        {
            @Override
            public <T2> T2 getValue(Expression<T2> dependency) {
                registerDependency(expression, dependency);
                return eval(dependency);
            }
        });
        cache.put(weak, result);
    }
    return result;
}

public void update(Expression<?> ex) {
    changed(weaken(ex));
}

public void changed(Weak weak) {
    cache.remove(weak);

    dependencies.removeOutgoingArcs(weak);
    for(Weak dependant : new ArrayList<Weak>(dependencies.getIncoming(weak))) {
        changed(dependant);
    }
}

キャッシュ マネージャーがオブジェクトを要求されると、最初にキャッシュをチェックします。キャッシュに値がない場合は、式に評価を求めます。次に式は、getValue() メソッドを呼び出して依存関係を解決するようにキャッシュ マネージャーに要求します。これにより、ディペンデンシー グラフに円弧が作成されます。このグラフは、後でキャッシュの無効化に使用されます。

式が無効化されると、依存関係グラフが調査され、すべての依存キャッシュが無効化されます。

キャッシュと依存グラフのクリーンアップは、ガベージ コレクターが (ReferenceQueue を介して) 一部の式オブジェクトの終了を通知するとすぐに実行されます。

ほとんどすべてが正常に機能します。ただし、いくつかのトリッキーなケースがあります。

トリッキーなケース

最初のケースは、ぶら下がっている中間依存関係です。次のクラスがあるとします。

class SumProxyExpression implements Expression<Integer> {
    private final Expression<Integer> a;
    private final Expression<Integer> b;

    public SumProxyExpression(Expression<Integer> a, Expression<Integer> b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer evaluate(EvaluationContext context) {
        Expression<Integer> s = new SumExpression(a, b);
        return context.getValue(s);
    }
}

のインスタンスを作成し、後でc=SumProxyExpression(a,b)値を変更する場合は、その値も変更する必要があります。ただし、中間体がすでにガベージ コレクションされている場合、これは発生しない可能性があります。これに対抗するために、ノードがリーフ ノードでない限り、ディペンデンシー グラフからノードを削除しません (着信アークのみまたは発信アークのみを持つ)。acSumExpression

解決方法がわからないもう1つのケースは次のとおりです。

class SelfReferencingExpression implements Expression<List<?>> {
    class Result extends ArrayList<Integer> {
    }

    @Override
    public List<?> evaluate(EvaluationContext resolver) {
        return new Result();
    }
}

そのような式の結果をキャッシュすると、キャッシュされた値 ( Result) へのハード参照を保持し、それを含むクラス (式) への参照があるため、ガベージ コレクションされることはありません。したがって、式は常に到達可能ですが、使用できませんでした。

これはメモリ リークであり、それを解消する方法がわかりません。そのような参照を決して持たないようにユーザーに伝えることは可能ですが、非常に危険なので、より良い解決策を見つけたいと思います.

代替ソリューション

また、すべてをグローバル キャッシュに保持するのではなく、共通のセルフ キャッシュ式クラスから継承して実装することも考えました。このソリューションは、最後のテスト ケース (SelfReferencingExpression) を解決しますが、最初のテスト ケース (SumProxyExpression) では失敗します。だから、私は何をすべきかわかりません。助けてください。

于 2010-11-14T16:06:00.033 に答える