- PVSM.RU - https://www.pvsm.ru -
Многие разработчики нередко встают перед дилеммой – получать данные только из базы или же держать кэш для ряда таблиц. В основном, это некоторые справочники, которые содержат мало записей и постоянно нужны под рукой. Вопрос этот холиварный и затрагиваться в данной статье не будет.
Такая же проблема встала и передо мной при проектировании высоконагруженного сервера системы мониторинга транспорта на .NET. В итоге, было принято решение, что кэшам – быть. Кэши словарей стали храниться в обёртках над ConcurrentDictionary. Этот вариант был взят без особых исследований, поскольку является стандартным средством .NET для потокобезопасных словарей. Теперь настало время проверить производительность данного решения. Об этом, собственно, статья. Также в конце статьи будет небольшое исследование того, как устроен внутри ConcurrentDictionary.
Постановка задачи
Требуется потокобезопасная коллекция пар ключ-значение, которая умеет следующее:
Пункт 3 нетипичен для коллекций-словарей и потому является главным тормозом данной конструкции. Однако, если используется кэширование справочных таблиц, подобные ситуации неизбежны (например, банально потребуется вывести весь словарь в админке для редактирования).
Попробуем рассмотреть разные системы, в которых будет использоваться кэш. Отличаться они будут частотой операций со словарём. Формализуем типы этих операций:
Список участников тестирования
Кратко расскажу, как именно реализованы обработчики.
Тестовый стенд
Для тестирования создано консольное приложение (ссылка [9]).
Значения по умолчанию достаточно маленькие, однако, если их увеличивать, принципиально ничего не меняется.
Результаты тестирования
Тестирование производилось с различными вариантами распределения по типам операций и таблица получилась слишком большой, посмотреть её целиком можно здесь (ссылка [10]). Для наглядности приведу график времени выполнения теста в микросекундах в зависимости от процента чтения по значению от общего числа операций (операций записи при этом фиксировано 20%, остальные – чтение по ключу).
Выводы
Производительность всех участников падает линейно от количества чтений по значению не зависимо от количества операций записи.
Внутреннее устройство ConcurrentDictionary
Присмотримся к победителю поподробнее, а именно: посмотрим исходники ConcurrentDictionary.
При создании этого класса задаются 2 параметра: Capacity и ConcurrencyLevel. Первый (Capacity) является привычным для коллекций и задаёт количество элементов, которые могут быть записаны без расширения внутренних коллекций. В данном случае создаются связанные списки (m_buckets, поэтому далее будем называть их корзинами (ну не вёдрами же, да?)) в количестве Capacity, а затем элементы относительно равномерно в них добавляются. Значение по умолчанию — 31.
Второй параметр (ConсurrencyLevel) определяет количество потоков, которые могут одновременно писать в наш словарь. Реализуется это с помощью создания коллекции объектов для блокировки мониторами. Каждый такой объект блокировки отвечает за приблизительно одинаковое количество корзин. Значение по умолчанию — Environment.ProcessorCount * 4.
Таким образом, каждому объекту в словаре однозначно сопоставляется корзина, где он лежит, и объект блокировки для записи. Делается это следующим методом:
/// <summary>
/// Computes the bucket and lock number for a particular key.
/// </summary>
private void GetBucketAndLockNo(
int hashcode, out int bucketNo, out int lockNo, int bucketCount, int lockCount)
{
bucketNo = (hashcode & 0x7fffffff) % bucketCount;
lockNo = bucketNo % lockCount;
Assert(bucketNo >= 0 && bucketNo < bucketCount);
Assert(lockNo >= 0 && lockNo < lockCount);
}
Любопытно, что даже при ConcurrencyLevel = 1 ConcurrentDictionary всё равно работает быстрее, чем обычный словарь с локом. Также стоит отметить, что класс оптимизирован под использование через итератор (что и показали тесты). В частности, при вызове метода ToArray() осуществляется блокировка по всем корзинам, а итератор используется относительно дёшево.
Для примера: лучше использовать dictionary.Select(x => x.Value).ToArray(), чем dictionary.Values.ToArray().
Статья написана ведущим разработчиком компании.
Спасибо за внимание!
Автор: scoutgps
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/net/17998
Ссылки в тексте:
[1] ConcurrentDictionary : http://msdn.microsoft.com/en-us/library/dd287191.aspx
[2] Dictionary: http://msdn.microsoft.com/en-us/library/xfhwa508.aspx
[3] Monitor: http://msdn.microsoft.com/en-us/library/System.Threading.Monitor.aspx
[4] ReaderWriterLock: http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlock.aspx
[5] ReaderWriterLockSlim: http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx
[6] Wintellect: http://www1.wintellect.com/Resources/Details/76
[7] Джефри Рихтера: http://ru.wikipedia.org/wiki/Рихтер,_Джеффри
[8] Hashtable.Synchronized: http://msdn.microsoft.com/en-us/library/system.collections.hashtable.synchronized.aspx
[9] ссылка: http://yadi.sk/d/ExAA7H2w0O3Xh
[10] ссылка: http://yadi.sk/d/n5ycSsDf0O3Sf
Нажмите здесь для печати.