Автор: Sergey Teplyakov

У использования ConcurentDictionary есть одна особенность: в некоторых случаях он может вести себя не совсем так, как вы того ожидаете. Вот небольшой пример. Допустим, нам нужно запилить небольшой кэш, чтобы результаты дорогостоящей операции брались из кэша, если они там есть, ну и добавлялись в кэш прозрачным образом, если Акела промахнулся.

Простая реализация некоторого провайдера с cache aside паттерном будет выглядеть примерно так:public class CustomProvider
{
private readonly ConcurrentDictionary<,string, OperationResult>, _cache =
new ConcurrentDictionary<,string, OperationResult>,
(),
public OperationResult RunOperationOrGetFromCache(string operationId)
{
return _cache.GetOrAdd(operationId, id =>, RunLongRunningOperation(id)),
}
private OperationResult RunLongRunningOperation(string operationId)
{
// Running real long-running operation
// ...
Thread.Sleep(10
),
Console.WriteLine('Running long-running operation'),
return OperationResult.Create(operationId),
}
}

С точки зрения многопоточности эта реализация совершенно корректна. Даже если метод RunOperationOrGetFromCache для одного и того же operationId будет вызван из двух потоков, то каждый из них получит один и тот же результат. Проблема же в том, что хоть результат и будет один и тот же, но запущено будет две операции. Результат первой будет помещен в кэш, а результат второй операции выброшен!

Причина в реализации метода AddOrGet класса ConcurrentDictionary. По сути, использование AddOrGet эквивалентно последовательному использованию методов TryGetValue и TryAdd в нашем собственном коде (метод AddOrGet немногим сложнее, чем простой вызов этих двух методов):public OperationResult RunOperationOrGetFromCache(string operationId)
{
OperationResult result,
if (_cache.TryGetValue(operationId, out result))
{
return result,
}
result
= RunLongRunningOperation(operationId),
_cache
.TryAdd(operationId, result),
return result,
}

Теперь должно быть понятно, что если два потока будут бодаться и одновременно получить результаты одной и той же операции, то длительная операция будет запущена дважды.

Однако, не все так плохо. Поскольку при конкурентном вызове метода AddOrGet лишь первый результат будет помещен в коллекцию, то можно воспользоваться следующим трюком:ConcurrentDictionary<,string, Lazy<,OperationResult>,>, _cache =
new ConcurrentDictionary<,string, Lazy<,OperationResult>,>,
(),
public OperationResult RunOperationOrGetFromCache(string
operationId)
{
return _cache.GetOrAdd(operationId,
id
=>, new Lazy<,OperationResult>,(() =>, RunLongRunningOperation(id))).Value,
}

Вместо непосредственно хранения результата длительной операции, кэш хранит ленивую оболочку - Lazy<,OperationResult>,. В этом случае, при одновременном обращении к кэшу из нескольких потоков несколько раз будет вызван лишь конструктор объекта Lazy<,T>,, а непосредственно операция будет запущена лишь один раз при обращении к свойству Value!

Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).