Проблемы с GC при использовании WeakValueDictionary для кешей

Согласно официальной документации Python для модуля weakref, «основное использование слабых ссылок — реализация кешей или отображений, содержащих большие объекты…». Итак, я использовал WeakValueDictionary для реализации механизма кэширования долго выполняющейся функции. Однако, как оказалось, значения в кеше никогда не оставались там до тех пор, пока они действительно не использовались снова, и их приходилось пересчитывать почти каждый раз. Поскольку сильных ссылок между обращениями к значениям, хранящимся в WeakValueDictionary, не было, сборщик мусора избавился от них (хотя проблем с памятью не было абсолютно).

Теперь, как я должен использовать слабые ссылки для реализации кэша? Если я оставлю сильные ссылки где-то явно, чтобы сборщик мусора не удалил мои слабые ссылки, в первую очередь не было бы смысла использовать WeakValueDictionary. Вероятно, должна быть какая-то опция для GC, которая говорит ему: удалять все, на что вообще нет ссылок, и все со слабыми ссылками, только когда заканчивается память (или превышен какой-то порог). Есть ли что-то подобное? Или есть лучшая стратегия для такого типа кеша?


person Elmar Zander    schedule 13.03.2012    source источник


Ответы (2)


Я попытаюсь ответить на ваш вопрос примером того, как использовать модуль weakref для реализации кэширования. Мы будем хранить слабые ссылки нашего кеша в weakref.WeakValueDictionary, а сильные ссылки в collections.deque, потому что у него есть свойство maxlen, которое контролирует, сколько объектов он удерживает. Реализовано в стиле закрытия функции:

import weakref, collections
def createLRUCache(factory, maxlen=64):
    weak = weakref.WeakValueDictionary()
    strong = collections.deque(maxlen=maxlen)

    notFound = object()
    def fetch(key):
        value = weak.get(key, notFound)
        if value is notFound:
            weak[key] = value = factory(key)
        strong.append(value)
        return value
    return fetch

Объект deque будет хранить только последние maxlen записей, просто удаляя ссылки на старые записи, как только он достигнет своего предела. Когда старые записи будут удалены, а python соберет мусор, WeakValueDictionary удалит эти ключи с карты. Следовательно, комбинация двух объектов помогает нам хранить только maxlen записей в нашем кэше LRU.

class Silly(object):
    def __init__(self, v):
        self.v = v

def fib(i):
    if i > 1:
        return Silly(_fibCache(i-1).v + _fibCache(i-2).v)
    elif i: return Silly(1)
    else: return Silly(0)
_fibCache = createLRUCache(fib)
person Shane Holloway    schedule 13.03.2012

Похоже, это ограничение невозможно обойти, по крайней мере, в CPython 2.7 и 3.0.

Размышляя над решением createLRUCache():

Решение с createLRUCache(factory, maxlen=64) не соответствует моим ожиданиям. Идея привязки к «maxlen» — это то, чего я хотел бы избежать. Это заставило бы меня указать здесь какую-то немасштабируемую константу или создать некоторую эвристику, чтобы решить, какая константа лучше подходит для тех или иных ограничений памяти хоста.

Я бы предпочел, чтобы GC удалял неиспользуемые значения из WeakValueDictionary не сразу, а при условии используется для обычного GC:

Когда количество выделений за вычетом количества освобождений превышает пороговое значение 0, начинается сбор.

person Oleksiy    schedule 02.12.2014