Подтвердить что ты не робот

Уплотнение словаря слабых ссылок

У меня есть класс Foo с идентификатором свойства. Моя цель состоит в том, что нет двух экземпляров Foo с одним и тем же идентификатором одновременно.

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

static Foo CreateFoo(int id) {
    Foo foo;
    if (!cache.TryGetValue(id, out foo)) {
        foo = new Foo(id);
        foo.Initialize(...);
        cache.Put(id, foo);
    }
    return foo;
}

Кэш реализован в виде словаря < TKey, WeakReference > , на основе @JaredPar Построение таблиц WeakReference Hashtable:

class WeakDictionary<TKey, TValue> where TValue : class {
    private readonly Dictionary<TKey, WeakReference> items;
    public WeakDictionary() {
        this.items = new Dictionary<TKey, WeakReference>();
    }
    public void Put(TKey key, TValue value) {
        this.items[key] = new WeakReference(value);
    }
    public bool TryGetValue(TKey key, out TValue value) {
        WeakReference weakRef;
        if (!this.items.TryGetValue(key, out weakRef)) {
            value = null;
            return false;
        } else {
            value = (TValue)weakRef.Target;
            return (value != null);
        }
    }
}

Проблема в том, что WeakReferences остаются в словаре после того, как их цели были собраны в мусор. Это подразумевает необходимость в некоторой стратегии, как вручную "мусор собирать" мертвые WeakReferences, как объяснил @Pascal Cuoq в Что происходит с WeakReference после GC WeakReference.Target.


Мой вопрос: Какая лучшая стратегия для компактного словаря слова WeakReference?

Параметры, которые я вижу, следующие:

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

  • Пройдите весь словарь на каждом Put и TryGetValue и удалите мертвые WeakReferences. Это несколько портит цель словаря, потому что обе операции становятся O (n).

  • Периодически перемещайте весь словарь в фоновом потоке. Что было бы хорошим интервалом, учитывая, что я не знаю шаблон использования CreateFoo?

  • Добавить каждый вставленный KeyValuePair в список с двойным соединением. Каждый вызов Put и TryGetValue проверяет главу списка. Если WeakReference жив, переместите пару в конец списка. Если он мертв, удалите пару из списка и WeakReference из Словаря.

  • Реализовать пользовательскую хеш-таблицу с незначительной разницей, что, когда ведро заполнено, мертвые WeakReferences сначала удаляются из ведра, прежде чем продолжить, как обычно.

Существуют ли другие стратегии?

Лучшая стратегия - это, вероятно, алгоритм с усредненной временной сложностью. Существует ли такая стратегия?

4b9b3361

Ответ 1

Ваш вариант 3 (Thread) имеет большой недостаток в синхронизации, необходимой для всех действий Put/TryGetvalue. Если вы используете это, ваш интервал не в миллисекундах, а в каждом действии N TryGet.

Вариант 2, сканирующий словарь, повлечет за собой серьезные накладные расходы. Вы можете улучшить только сканирование 1 в 1000 действиях и/или просмотр того, как часто выполняется GC.

Но я бы серьезно рассмотрел вариант 1: ничего не делать. У вас может быть "много" мертвых записей, но, с другой стороны, они довольно малы (и перерабатываются). Вероятно, это не вариант для приложения Server, а для клиентского приложения, я попытался бы узнать, сколько записей (kByte) в час мы говорим.

После некоторого обсуждения:

Означает ли такая стратегия [n амортизация] существуют?

Думаю, нет. Ваша проблема - миниатюрная версия GC. Вам придется сканировать все это время от времени. Таким образом, только варианты 2) и 3) обеспечивают реальное решение. И они оба дороги, но они могут быть (сильно) оптимизированы с некоторыми эвристиками. Вариант 2) все равно даст вам случайный худший случай.

Ответ 2

Если вы можете переключить управляемый объект на ключ словаря, вы можете использовать .Net 4.0 ConditionalWeakTable (пространство имен System.Runtime.CompilerServices).

По словам г-на Рихтера, ConditionalWeakTable уведомляется об объединении объектов сборщиком мусора, а не использует поток опроса.

    static ConditionalWeakTable<TabItem, TIDExec> tidByTab = new ConditionalWeakTable<TabItem, TIDExec>();

    void Window_Loaded(object sender, RoutedEventArgs e)
    {
        ...
        dataGrid.SelectionChanged += (_sender, _e) =>
        {
            var cs = dataGrid.SelectedItem as ClientSession;

            this.tabControl.Items.Clear();

            foreach (var tid in cs.GetThreadIDs())
            {
                tid.tabItem = new TabItem() { Header = ... };
                tid.tabItem.AddHandler(UIElement.MouseDownEvent,
                    new MouseButtonEventHandler((__sender, __e) =>
                    {
                        tabControl_SelectionChanged(tid.tabItem);
                    }), true);
                tidByTab.Add(tid.tabItem, tid);
                this.tabControl.Items.Add(tid.tabItem);
            }
        };
    }

    void tabControl_SelectionChanged(TabItem tabItem)
    {
        this.tabControl.SelectedItem = tabItem;
        if (tidByTab.TryGetValue(tabControl.SelectedItem as TabItem, out tidExec))
        {
            tidExec.EnsureBlocksLoaded();
            ShowStmt(tidExec.CurrentStmt);
        }
        else
            throw new Exception("huh?");
    }

Важно то, что единственное, что ссылается на объект TabItem, - это коллекция tabControls.Items и ключ ConditionalWeakTable. Ключ ConditionalWeakTable не учитывается. Поэтому, когда мы очищаем все элементы из tabControl, эти теги TabItems могут быть собраны в мусор (поскольку ничто не ссылается на них больше, снова ключ ConditionalWeakTable не учитывается). Когда они собираются в гарабе, ConditionalWeakTable уведомляется и запись с этим значением ключа удаляется. Таким образом, мои громоздкие объекты TIDExec также собираются с мусором в этой точке (ничего не ссылается на них, кроме значения ConditionalWeakTable).

Ответ 3

Подход № 5 интересен, но имеет тот недостаток, что может быть трудно понять, каков реальный уровень использования хеш-таблицы, и, следовательно, когда хэш-таблица должна быть расширена. Эта трудность может быть преодолена, если, когда она "кажется", как хэш-таблица, должна быть расширена, сначала сначала выполняется сканирование целых столов для удаления мертвых записей. Если более половины записей в таблице были мертвы, не беспокойтесь об этом. Такой подход должен приводить к амортизированному поведению O (1), поскольку вы не выполняли бы проверку всего стола, пока не добавили бы столько записей, которые были удалены.

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

Ответ 4

У меня была такая же проблема, и я решил это так (WeakDictionary - это класс, который я пытался очистить):

internal class CleanerRef
{
    ~CleanerRef()
    {
        if (handle.IsAllocated)
            handle.Free();
    }

    public CleanerRef(WeakDictionaryCleaner cleaner, WeakDictionary dictionary)
    {
        handle = GCHandle.Alloc(cleaner, GCHandleType.WeakTrackResurrection);
        Dictionary = dictionary;
    }

    public bool IsAlive
    {
        get {return handle.IsAllocated && handle.Target != null;}
    }

    public object Target
    {
        get {return IsAlive ? handle.Target : null;}
    }

    GCHandle handle;
    public WeakDictionary Dictionary;
}


internal class WeakDictionaryCleaner
{
    public WeakDictionaryCleaner(WeakDictionary dict)
    {
        refs.Add(new CleanerRef(this, dict));
    }

    ~WeakDictionaryCleaner()
    {
        foreach(var cleanerRef in refs)
        {
            if (cleanerRef.Target == this)
            {
                cleanerRef.Dictionary.ClearGcedEntries();
                refs.Remove(cleanerRef);
                break;
            }
        }
    }
    private static readonly List<CleanerRef> refs = new List<CleanerRef>();
}

То, что эти два класса пытаются достичь, - это "зацепить" GC. Вы активируете этот механизм, создав экземпляр WeakDictionaryCleaner во время построения слабой коллекции:

new WeakDictionaryCleaner(weakDictionary);

Обратите внимание, что я не создаю ссылку на новый экземпляр, так что GC будет утилизировать его в течение следующего цикла. В методе ClearGcedEntries() я снова создаю новый экземпляр, так что каждый цикл GC будет содержать очиститель, который, в свою очередь, выполнит уплотнение коллекции. Вы можете сделать CleanerRef.Dictionary также слабой ссылкой, чтобы он не удерживал словарь в памяти.

Надеюсь, что это поможет

Ответ 5

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

static IdentityMap<int, Entity> Cache = new IdentityMap<int, Entity>(e => e.ID);
...
var entity = Cache.Get(id, () => LoadEntity(id));

Класс предоставляет только один открытый метод Get с key и необязательным параметром value, который лениво загружает и кэширует объект, если он не находится в кеше.

using System;
class IdentityMap<TKey, TValue>
    where TKey : IEquatable<TKey>
    where TValue : class
{
    Func<TValue, TKey> key_selector;
    WeakReference<TValue>[] references;
    int[] buckets;
    int[] bucket_indexes;
    int tail_index;
    int entries_count;
    int capacity;

    public IdentityMap(Func<TValue, TKey> key_selector, int capacity = 10) {
        this.key_selector = key_selector;
        Init(capacity);
    }
    void Init(int capacity) {
        this.bucket_indexes = new int[capacity];
        this.buckets = new int[capacity];
        this.references = new WeakReference<TValue>[capacity];
        for (int i = 0; i < capacity; i++) {
            bucket_indexes[i] = -1;
            buckets[i] = i - 1;
        }
        this.tail_index = capacity - 1;
        this.entries_count = 0;
        this.capacity = capacity;
    }

    public TValue Get(TKey key, Func<TValue> value = null) {
        int bucket_index = Math.Abs(key.GetHashCode() % this.capacity);
        var ret = WalkBucket(bucket_index, true, key);
        if (ret == null && value != null) Add(bucket_index, ret = value());
        return ret;
    }

    void Add(int bucket_index, TValue value) {
        if (this.entries_count == this.capacity) {
            for (int i = 0; i < capacity; i++) WalkBucket(i, false, default(TKey));
            if (this.entries_count * 2 > this.capacity) {
                var old_references = references;
                Init(this.capacity * 2);
                foreach (var old_reference in old_references) {
                    TValue old_value;
                    if (old_reference.TryGetTarget(out old_value)) {
                        int hash = key_selector(value).GetHashCode();
                        Add(Math.Abs(hash % this.capacity), old_value);
                    }
                }
            }
        }
        int new_index = this.tail_index;
        this.tail_index = buckets[this.tail_index];
        this.entries_count += 1;
        buckets[new_index] = bucket_indexes[bucket_index];
        if (references[new_index] != null) references[new_index].SetTarget(value);
        else references[new_index] = new WeakReference<TValue>(value);
        bucket_indexes[bucket_index] = new_index;
    }

    TValue WalkBucket(int bucket_index, bool is_searching, TKey key) {
        int curr_index = bucket_indexes[bucket_index];
        int prev_index = -1;
        while (curr_index != -1) {
            TValue value;
            int next_index = buckets[curr_index];
            if (references[curr_index].TryGetTarget(out value)) {
                if (is_searching && key_selector(value).Equals(key)) return value;
                prev_index = curr_index;
            } else {
                if (prev_index != -1) buckets[prev_index] = next_index;
                else bucket_indexes[bucket_index] = next_index;

                buckets[curr_index] = this.tail_index;
                this.tail_index = curr_index;
                this.entries_count -= 1;
            }
            curr_index = next_index;
        }
        return null;
    }
}

Ответ 6

Вы можете удалить "недействительный" WeakReference внутри TryGetValue:

[Изменить] Моя ошибка, эти решения фактически не более того, что вы предложили, так как метод Put поменяет старый объект на новый. Просто проигнорируйте это.

public bool TryGetValue(TKey key, out TValue value) {
    WeakReference weakRef;
    if (!this.items.TryGetValue(key, out weakRef)) {
        value = null;
        return false;
    } else {
        value = (TValue)weakRef.Target;
        if (value == null)
            this.items.Remove(key);
        return (value != null);
    }
}

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

public TValue GetOrCreate(TKey key, Func<Tkey, TValue> ctor) {

    WeakReference weakRef;
    if (!this.items.TryGetValue(key, out weakRef) {
        Tvalue result = ctor(key);
        this.Put(key, result);
        return result;
    } 

    value = (TValue)weakRef.Target;
    if (value == null)
    {
        Tvalue result = ctor(key);
        this.Put(key, result);
        return result;
    }

    return value;
}

Затем вы будете использовать его следующим образом:

static Foo CreateFoo(int id)
{
    return cache.GetOrCreate(id, id => new Foo(id));
}

[Изменить]

Согласно windbg, экземпляр WeakReference только один занимает 16 байт. Для 100 000 собранных объектов это не будет таким серьезным бременем, поэтому вы можете легко позволить им жить.

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

Ответ 7

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

public class Entry<TKey>
{
    TKey key;
    Dictionary<TKey, WeakReference> weakDictionary;

    public Entry(Dictionary<TKey, WeakReference> weakDictionary, TKey key)
    {
        this.key = key;
        this.weakDictionary = weakDictionary;
    }

    ~Entry()
    {
        weakDictionary.Remove(key);
    }
}

Когда кэшированные объекты являются подклассом Entry<TKey>, пустые WeakReference просачиваются, поскольку finalyzer вызывается после того, как его экземпляр был собран сборщиком мусора.