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

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

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

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)

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

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

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

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

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

Другие вопросы по тегам