2015-10-20 2 views
3

У меня есть кэш (который используется веб-приложением), который использует два кэша - один кратковременный кеш, который используется только в запросе, и долгосрочный кеш, который используется «постоянно», (через запросы).Стоимость блокировки

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

public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc) 
{ 
    TCache cacheItem; 
    if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)) 
    { 
     return cacheItem; 
    } 

    DateTime cacheDependancyLastModified; 
    if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem) 
     && IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified)) 
    { 
     cacheItem.CacheTime = cacheDependancyLastModified; 
     shortTermCache[cachdeDependancy.Id] = cacheItem; 
     return cacheItem; 
    } 

    cacheItem = cacheItemCreatorFunc(cachdeDependancy); 

    longTermCache.Add(cachdeDependancy.Id, cacheItem); 
    shortTermCache[cachdeDependancy.Id] = cacheItem; 
    return cacheItem; 
} 

Очевидно, что это еще возможно (возможно, даже вероятно), что приведенный выше код не будет соответствовать при выполнении параллельных (то есть несколько веб-запросов). Однако я написал несколько модульных тестов, и я увидел, что никогда не возникает «исключение». Может случиться так, что тот же элемент добавляется снова, даже если он уже существует и т. Д. -> Я думаю, вы можете видеть, что я имею в виду, когда вы смотрите на код.

Тем не менее я думал, что было бы неплохо иметь решение, которое всегда работает правильно и согласовано.

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

public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc) 
{ 
    TCache cacheItem; 
    if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)) 
    { 
     return cacheItem; 
    } 

    lock (_lockObj) 
    { 
     if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)) 
     { 
      return cacheItem; 
     } 

     DateTime cacheDependancyLastModified; 
     if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem) 
      && IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified)) 
     { 
      cacheItem.CacheTime = cacheDependancyLastModified; 
      shortTermCache[cachdeDependancy.Id] = cacheItem; 
      return cacheItem; 
     } 

     cacheItem = cacheItemCreatorFunc(cachdeDependancy); 

     longTermCache.Add(cachdeDependancy.Id, cacheItem); 
     shortTermCache[cachdeDependancy.Id] = cacheItem; 
     return cacheItem; 
    } 
} 

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

Однако я не уверен: Будет ли это не слишком медленным и, следовательно, также разрушает цель кеша? Может быть, было бы лучше жить с проблемой, что кеш может иногда иметь «непоследовательное» поведение? Потому что, если есть 1000 веб-запросов одновременно, все должны подождать, пока они не смогут войти в зону блокировки. Или это совсем не проблема, потому что процессор имеет только определенное количество ядер (и, следовательно, «настоящих» параллельных потоков), и это ограничение производительности всегда будет незначительным?

+2

Это много зависит от 2 думает: сколько потоков cuncurrent обращается к вашему коду и сколько времени требуется для запуска кода в инструкции блокировки. До тех пор, пока он получает доступ только к памяти (нет db, никакого диска) и методы, которые вы вызываете, бывают быстрыми, это не должно быть реальной проблемой. (если ваш 1000-й запрос должен ждать миллисекунду или около того, это не будет узким местом) Но опять-таки, это зависит от реального сценария. –

+0

Вы можете использовать [Закон Амдаля] (https://de.wikipedia.org/wiki/Amdahlsches_Gesetz), чтобы рассчитать улучшение путем распараллеливания. – Markus

+0

Одновременно может создаваться только один элемент кэша, который кажется убийцей. – usr

ответ

4

Если вы используете ConcurrentDictionary, у вас уже есть способ сделать то, что вы хотите - вы можете просто использовать GetOrAdd метод:

shortTermCache[cacheDependency.Id] = 
    longTermCache.GetOrAdd(cacheDependency.Id, _ => cacheItemCreatorFunc(cachdeDependancy)); 

Быстро и легко :)

Вы даже можете расширить этот включить короткую проверку термин кэша:

return 
    shortTermCache.GetOrAdd 
    (
    cacheDependency.Id, 
    _ => 
    { 
     return longTermCache 
      .GetOrAdd(cacheDependency.Id, __ => cacheItemCreatorFunc(cacheDependency)); 
    } 
); 

Хотя это своего рода ненужным использование ConcurrentDictionary для кэша каждого запроса - это Безразлично» t действительно должны быть потокобезопасными.

Что касается вашего исходного кода, то он сломан. Тот факт, что вы не видите, что во время тестирования не слишком удивителен - проблемы многопоточности часто трудно воспроизвести. Вот почему вы хотите, чтобы код был правильно, и это означает, что вы должны понимать, что именно происходит, и какие могут быть проблемы с параллелизмом. В вашем случае есть две общие ссылки: longTermCache и сама cacheItem. Даже если все объекты, с которыми вы работаете, являются потокобезопасными, у вас нет гарантии, что ваш код также является потокобезопасным - в вашем случае может возникнуть спор по поводу cacheItem (как поточно-безопасный это?), или кто-то мог добавить тот же элемент кэша тем временем.

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

Ваше обновление с добавленным lock устраняет эти проблемы. Тем не менее, он не справляется с тем, как вы, например, утечка cacheItem. Если cacheItem отлично подходит для потоков, вы можете столкнуться с некоторыми трудностями для отслеживания ошибок. И мы уже знаем, что это не является неизменным - по крайней мере, вы меняете время кеша.

+0

спасибо за ответ. Tbh Я уже использую только Dictinoary для ShortTermCache - я просто хочу d предотвратить обсуждения, об использовании «потоковых безопасных структур данных» где угодно;) Что мне действительно также интересно (если даже можно сделать заявление), как вы думаете, что вся «блокирующая» вещь будет иметь разницу в производительности. –

+0

@ OschtärEi Это не вопрос, на который нельзя ответить в целом. Вполне возможно, что будет нулевой эффект - или это может привести к остановке всего вашего приложения. Почему вы даже поддерживаете локальный кеш запроса? – Luaan

+0

Я так и думал ... ну, метод 'IsStillValid' занимает немного времени, поэтому я хочу только сделать это один раз в запросе, а затем использовать кратковременный кеш для дальнейшего доступа. –

Смежные вопросы