Можно ли улучшить работу этого метода ?

Pruvetik
Дата: 05.05.2014 14:28:18
Привет.

Помогите оптимизировать метод.


Использую ConcurrentDictionary<string, string> в многопоточной програмке.
Мне нужно просто периодически (раз в 10 минут) пройтись по коллекции и удалить из нее элементы, которые не проходят по условию (если конкретно - мне просто нужно чистить словарик от устаревших записей, которые никто уже не запросит и они так и будут висеть вечно, если их не удалить).
Проверка условия происходит по value а не key. Если бы проверка происходила по ключу, то проблемы бы конечно не было бы.
Я конечно могу запросить массив values и пройтись по нему, но найдя те, что нужно удалить, я не имею ссылки на ключ.

Таким образом я пришел к такому решению:

            var keys = myDict.Keys;
            foreach (string key in keys)
            {
                string str;
                if (myDict.TryGetValue(key, out str))
                {                    
                    if ( условие )
                        myDict.TryRemove(key, out str /*значение str нам уже не интересно, но прототип функции таков*/);
                }
                
            }



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


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

В LINQ дофига полезных методов, для работы с коллекциями, может какой-то может помочь в моей задаче ?
Хотя не уверен, что IEnumerable корректно заточен для работы с многопоточными коллекциями.
Как известно запрещено изменять коллекцию, по которой идет энумератор (т.е. нельзя идти внутри foreach и тут же в цикле удалять элементы из проходимого перечисления) - но т.к. речь идет об ConcurrentDictionary, то удалить элемент может кто-то другой.

Можно ли как-то получить сразу все значения KeyValuePair из словаря, чтобы спокойно их проверить (а не только в цикле foreach)? Это бы полностью решило проблему. Но насколько я знаю эту структуру возвращает лишь метод Enumerator.Current, т.е. придется идти по всему циклу - а как я написал выше, я опасаюсь использовать энумератор в конкурентных структурах.
ЕвгенийВ
Дата: 05.05.2014 15:08:11
Pruvetik

Можно ли как-то получить сразу все значения KeyValuePair из словаря, .

ToList()
Pruvetik
Дата: 05.05.2014 15:27:25
ЕвгенийВ,

Видел этот метод, не мог понять как его использовать на словаре, все думал, что же он вернет - и пример кода не отвечал. А выходит, что именно KeyValuePair он и возвращает ? Это просто замечательно и решает все проблемы.

Вопрос лишь по потокозащищенности !
Ведь этот метод является лишь методом-расширения. Т.е. он наверное нифига не учитывает внутренние подробности структуры. Скорее всего там тупо идет проход по перечислению и формируется список.

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

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

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


Ладно, спасибо большое, буду пробовать.
petalvik
Дата: 05.05.2014 15:47:25
Pruvetik
Вопрос лишь по потокозащищенности !

Вместо ToList (который является методом расширения) следует использовать родной метод ToArray (есть с таким же названием и метод расширения).

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

Внутри ConcurrentDictionary используются LinkedList. И насколько я помню, итератор, проходящий по этому списку не портится, когда другой поток удаляет элемент. То есть особых проблем быть не должно.

А почему бы не использовать MemoryCache? В нём можно гибко задавать политики, когда следует удалять ставший ненужным элемент. И он тоже потокобезопасный.
ЕвгенийВ
Дата: 05.05.2014 16:47:32
petalvik
Внутри ConcurrentDictionary используются LinkedList.

Хештаблица!
petalvik
Дата: 05.05.2014 17:00:37
ЕвгенийВ
petalvik
Внутри ConcurrentDictionary используются LinkedList.

Хештаблица!

Сам словарь является хэштаблицей. А каждый bucket внутри является linked list'ом.
Pruvetik
Дата: 05.05.2014 17:31:54
petalvik,

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

Позже, как разберусь с другими задачами, изучу MemoryCache.
Можно ли там настроить не только время, но и условие удаления по значению value ?
Если там можно передать Func<...,bool> который по таймеру будет применен к каждому элементу, и вернет "его можно/нельзя удалить" то это замечательно - мое решение примерно так и работает сейчас.

Ладно, по гуглю примеры кода потом.
mikron
Дата: 05.05.2014 18:03:44
Pruvetik,

В реализации твоего метода есть один "косяк", который может поломатъ всю многопоточность:
В момент удалеия нет гарантии что словарь содержит всё то-же значение, которое ты проверял ранее.
Если другие потоки могут (удалять и добавлять) или обновлять -> у тебя проблема в коде.
Pruvetik
Дата: 05.05.2014 18:29:47
mikron,

Вы верно заметили возможную проблему.

Но я изначально учитывал, что работа будет вестись в многопоточном режиме.
В словарик можно только добавлять и удалять, и ни в коем случае не редактировать Value (этого просто и не нужно в задаче). Ключами являются GUID'ы, как самый просто вариант. С int ключами была бы риск переполнения, рано или поздно, если программа работает 7х24.
Не хотелось усложнять с "зацикливанием" ключа (когда достигли int.MaxValue - начинаем с нуля), чтобы не получить риск совпадений ключей на втором круге, потому что их не удалили от первой итерации. Хотя такой ошибки не должно быть в принципе из-за, как раз, метода-чистильщика.
В общем, для безопасности и надежности выбрал гуиды - надеюсь вероятность их дублирования слишком мала, чтобы этого опасаться. В списке вряд ли будут единовременно находится больше 100 элементов. Не верю, что может случится так, что в этих 100 гуидах попадутся 2 идентичных. Это просто не реально надеюсь.

Нет риска того, что при проверке выяснилось, что "этот элемент списка можно удалять", а при самом моменте удаления удалится уже отредактированный элемент.
Pruvetik
Дата: 05.05.2014 18:43:08
mikron,

Проблемы "другой поток удалит, а потом чистильщик тоже попытается удалить" нет, т.к. это нормальный режим работы. Потому и используются TryGetValue, TryRemove.

Мне не важно кто из потоков в итоге удалит запись. Важно лишь, чтобы в списке не повисли мертвые записи - ибо через N дней этот список сожрет всю оперативку на компе.
Метод-чистильщик лишь проверяет что таких записей нет, и удаляет если найдет. Таймаут проверки - 10 минут, а образовываться такие "мертвые" записи будут редко. Нужно иметь пару милионов записей, чтобы это заняло какой то заметный объем памяти. Причем эта пара миллионов должна образоваться разом именно между вызовами метода чистильщика - т.к. он своим единственным проходом все почистит.

В целом, для безопасности, можно еще делать периодическую проверку свойства Count словаря (в отличие от IEnumerable проблем быть не должно совсем). Если она наберется хотя бы тысяч 100, то записать в лог информацию.
Потом можно будет сделать выводы о том, как часто такое происходит и допилить. В принципе мало ли, вдруг нагрузка на систему со временем возрастет, сделать проверочку не сложно.

запишу это в ToDo.