Взаимозависимость аннулирования кэшей и управления памятью - PullRequest
1 голос
/ 14 ноября 2010

Я работаю над проектом Java, который интенсивно использует шаблон Observer, чтобы убедиться, что каждое состояние объекта данных актуально. Я устал от поддержания этого беспорядка и пытаюсь реализовать решение, которое бы отделило ужасы шаблона Observer от моих драгоценных объектов данных.

Мне удалось абстрагироваться от деталей этого проекта, чтобы сказать, что проблема, которую я пытаюсь решить, следующая:

Существует множество объектов, представляющих выражения, каждый из которых может зависеть от значений других выражений.

Требуются следующие две операции:

eval (): получить значение заданного выражения

Эта операция должна возвращать актуальное значение выражения, которое будет возвращено, если все зависимости выражения будут пересмотрены прямо сейчас. Однако никакое выражение не должно оцениваться более одного раза, если только его кэш не аннулирован второй операцией:

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

ОК, функциональная часть закончена, здесь идет часть управления памятью.

У разработчика должен быть простой способ управления графом выражений. В идеале, распределение должно выполняться с new Sum(a,b), у разработчика должна быть свобода передавать экземпляры выражений по своему усмотрению без особых знаний о кеше, и освобождение должно происходить автоматически без каких-либо усилий со стороны разработчика.

И не должно быть никаких утечек памяти. То есть, когда выражение освобождается, в памяти не должно быть ничего, связанного с ним. Например, если шаблон аннулирования должен использоваться для аннулирования, выражение должно быть удалено из всех списков наблюдателей.

Вопрос:

Каким будет ваш подход к реализации этого на вашем любимом языке?

Также приветствуются не-сборочные и функциональные языки, особенно функциональные, потому что я вообще не понимаю, как подойти к этой проблеме в чистом функционале.

Лучшее решение, с моей точки зрения, было бы с наименьшей вероятностью ошибки разработчика.

Я намеренно не публикую свои текущие подробности реализации, потому что я думаю, что нашел фундаментальный недостаток в моей реализации, и я не вижу никакого пути вокруг этого. Я опубликую это позже все же.

Ответы [ 2 ]

1 голос
/ 25 декабря 2010

Если кому-то интересно (что, вероятно, никому не интересно), мне пришлось отказаться от идеи глобального кэша и решить проблему, сделав самошифрование Expression.

Я реализовал всю логику в базовом классе с именем ExpressionBase.

Решение включает следующее:

  • Выражение содержит список слабых ссылок наего иждивенцев и уведомляет их об изменениях.Таким образом, нет утечек памяти и нет необходимости отписываться.
  • Во время оценки выражения он автоматически обнаруживает зависимости способом, аналогичным описанному в моем предыдущем ответе, и подписывается на них.
  • СписокЗависимости сохранены для предотвращения слишком раннего сбора мусора промежуточных выражений (случай SumProxyExpression из моего предыдущего ответа).Таким образом, каждая слабая ссылка имеет свой обратный сильный аналог, так что цепочки слабых ссылок не разрываются GC, если только эти цепи не ведут в никуда.
0 голосов
/ 14 ноября 2010

ОК, здесь я попытаюсь объяснить свой подход к проблеме с использованием языка Java.

Все будет объяснено на примере SumExpression - выражения, используемого для сложения результатов двух других выражений вместе.

Код пользователя

Я начал с самого простого подхода - паттерна 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);
    }
}

Однако существует множество мест, где это может пойти не так, и что-то столь же простое, как сложение двух чисел, должно быть намного проще.Итак, я отделил логику от кеширования следующим образом:

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 имеет два аспекта: он извлекает значения из кэша и собирает список зависимостей между SumExpression и выражениями a и b.

Код ядра

Затем я предоставил EvaluationContext глобальным классом кэширования, который хранит кэшированные данные в структуре, аналогичной WeakHashMap<Expression, Object>, и данные графа зависимостей в группе обеспечения доступности баз данных с узлами типа WeakReference<Expression>.

Вот моя реализация eval и update :

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) и изменим значение на a позже, мы бы хотели, чтобы c также изменил его значение.Однако, если промежуточный SumExpression уже собран мусором, это может не произойти.Чтобы бороться с этим, я не удаляю узлы из графа зависимостей, если они не являются конечными узлами (имеют только входящие или только исходящие дуги).

Другой случай, который я не знаю, как решить, этоследующее:

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

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

Если я кеширую результат такого выражения, он никогда не будет собирать мусор, потому что я сохраняю жесткие ссылки на кэшированные значения (Result), и у него есть ссылка на содержащийкласс (выражение), поэтому выражение всегда достижимо, но никогда не может быть использовано.

Это утечка памяти, и я понятия не имею, как ее устранить.Сказать пользователю никогда не иметь такую ​​ссылку возможно, но очень опасно, поэтому я хотел бы найти лучшее решение.

Альтернативные решения

Я также думал о реализации его с наследованием от общегоКласс самокэшируемого выражения вместо хранения всего в глобальном кэше.Это решение решит последний контрольный пример (SelfReferencingExpression), но завершится неудачно с первым (SumProxyExpression).Итак, я не знаю, что делать.Пожалуйста, помогите.

...