.NET MemoryCache의 적절한 사용을위한 잠금 패턴 public static T

이 코드에는 동시성 문제가 있다고 가정합니다.

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

동시성 문제의 이유는 여러 스레드가 null 키를 얻은 다음 캐시에 데이터를 삽입하려고 할 수 있기 때문입니다.

이 코드 동시성 증명을 만드는 가장 짧고 깨끗한 방법은 무엇입니까? 캐시 관련 코드에서 좋은 패턴을 따르고 싶습니다. 온라인 기사에 대한 링크는 큰 도움이 될 것입니다.

최신 정보:

@Scott Chamberlain의 답변을 기반 으로이 코드를 생각해 냈습니다. 누구든지 이것으로 성능이나 동시성 문제를 찾을 수 있습니까? 이것이 작동하면 많은 코드 줄과 오류를 줄일 수 있습니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}



답변

이것은 코드의 두 번째 반복입니다. MemoryCache스레드로부터 안전 하기 때문에 초기 읽기를 잠글 필요가 없기 때문에 읽기만 할 수 있으며 캐시가 null을 반환하면 잠금 검사를 수행하여 문자열을 만들어야하는지 확인합니다. 코드를 크게 단순화합니다.

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

편집하다 : 아래 코드는 불필요하지만 원래 방법을 보여주기 위해 남겨두고 싶었습니다. 스레드 안전 읽기는 있지만 스레드 안전 쓰기가 아닌 다른 컬렉션을 사용하는 미래의 방문자에게 유용 할 수 있습니다 ( System.Collections네임 스페이스 아래의 거의 모든 클래스 가 비슷합니다).

다음은 ReaderWriterLockSlim액세스를 보호하기 위해 사용하는 방법 입니다. 잠그기 를 기다리는 동안 다른 사람이 캐시 된 항목을 생성했는지 확인하려면 일종의 ” 이중 확인 잠금 “을 수행해야합니다.

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}


답변

오픈 소스 라이브러리가 있습니다 [면책 조항 : 내가 작성한] : IMO가 두 줄의 코드로 귀하의 요구 사항을 처리 하는 LazyCache :

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey",
  () => SomeHeavyAndExpensiveCalculation());

기본적으로 잠금 기능이 내장되어 있으므로 캐시 가능한 메서드는 캐시 미스 당 한 번만 실행되며 람다를 사용하므로 한 번에 “가져 오기 또는 추가”를 수행 할 수 있습니다. 기본값은 20 분 슬라이딩 만료입니다.

심지어있다 NuGet 패키지 )


답변

MemoryCache 에서 AddOrGetExisting 메서드를 사용하고 Lazy 초기화를 사용 하여이 문제를 해결했습니다 .

기본적으로 내 코드는 다음과 같습니다.

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset);
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

여기서 최악의 시나리오는 동일한 Lazy개체를 두 번 만드는 것 입니다. 그러나 그것은 매우 사소합니다. AddOrGetExisting보장을 사용 하면 Lazy개체의 인스턴스를 하나만 얻을 수 있으므로 값 비싼 초기화 메서드를 한 번만 호출 할 수 있습니다.


답변

이 코드에는 동시성 문제가 있다고 가정합니다.

실제로는 개선이 가능하지만 상당히 괜찮습니다.

이제 일반적으로 획득 및 설정되는 값을 잠그지 않기 위해 처음 사용할 때 공유 값을 설정하는 여러 스레드가있는 패턴은 다음과 같습니다.

  1. 재앙-다른 코드는 하나의 인스턴스 만 존재한다고 가정합니다.
  2. 비참함-인스턴스를 얻는 코드는 하나 (또는 ​​특정 소수)의 동시 작업 만 허용 할 수 없습니다.
  3. 비참한-저장 수단은 스레드로부터 안전하지 않습니다 (예 : 사전에 두 개의 스레드가 추가되면 모든 종류의 불쾌한 오류가 발생할 수 있습니다).
  4. 차선-잠금으로 인해 하나의 스레드 만 값을 얻는 작업을 수행 한 경우보다 전체 성능이 더 나쁩니다.
  5. 최적-다중 스레드가 중복 작업을 수행하는 비용은 특히 비교적 짧은 기간 동안 만 발생할 수 있기 때문에이를 방지하는 비용보다 적습니다.

그러나 여기에서 MemoryCache항목을 제거 할 수 있음을 고려하면 다음과 같습니다.

  1. 두 개 이상의 인스턴스를 갖는 것이 재앙이라면 MemoryCache 잘못된 접근 방식입니다.
  2. 동시 생성을 방지해야하는 경우 생성 시점에서해야합니다.
  3. MemoryCache 해당 객체에 대한 액세스 측면에서 스레드로부터 안전하므로 여기서는 문제가되지 않습니다.

물론이 두 가지 가능성을 모두 고려해야하지만 동일한 문자열의 두 인스턴스가 존재하는 유일한 경우는 여기에 적용되지 않는 매우 특별한 최적화를 수행하는 경우입니다 *.

따라서 다음과 같은 가능성이 있습니다.

  1. 중복 호출 비용을 피하는 것이 더 저렴합니다. SomeHeavyAndExpensiveCalculation() .
  2. 에 대한 중복 호출 비용을 피하지 않는 것이 더 저렴합니다 SomeHeavyAndExpensiveCalculation().

그리고 그것을 해결하는 것은 어려울 수 있습니다 (실제로, 그것을 해결할 수 있다고 가정하는 것보다 프로파일 링 할 가치가있는 일종의 것입니다). 인서트 잠금의 가장 명백한 방법이 모든 것을 방지 할 수 있지만 여기서 고려할 가치가 있습니다. 은 관련이없는 것을 포함하여 캐시에 대한 추가를 .

즉, 50 개의 서로 다른 값을 설정하려는 50 개의 스레드가있는 경우 동일한 계산을 수행하지 않더라도 50 개의 스레드가 모두 서로를 대기하도록해야합니다.

따라서 경쟁 조건을 피하는 코드보다 보유한 코드를 사용하는 것이 더 낫습니다. 경쟁 조건이 문제인 경우 다른 곳에서 처리해야하거나 다른 코드가 필요합니다. 오래된 항목을 제거하는 것보다 캐싱 전략 †.

내가 바꾸고 싶은 것은 전화를 Set()하나로 바꾸는 것입니다.AddOrGetExisting() . 위에서부터 이것이 필요하지 않을 수도 있지만 새로 얻은 항목을 수집하여 전체 메모리 사용을 줄이고 저 세대 대 고 세대 컬렉션의 비율을 높일 수 있다는 것이 분명합니다.

예, 동시성을 방지하기 위해 이중 잠금을 사용할 수 있지만 동시성이 실제로 문제가되지 않거나 값을 잘못된 방식으로 저장하거나 저장소에 이중 잠금을 사용하는 것이 문제를 해결하는 가장 좋은 방법이 아닙니다. .

* 각 문자열 세트가 하나만 존재한다는 것을 알고 있다면 동등성 비교를 최적화 할 수 있습니다. 이는 문자열의 사본이 두 개있는 유일한 시간이 차선책이 아닌 부정확 할 수 있지만 그렇게하고 싶을 것입니다. 매우 다른 유형의 캐싱이 의미가 있습니다. 예를 들어 정렬 XmlReader은 내부적으로 수행됩니다.

† 무기한 저장하거나 약한 참조를 사용하여 기존 사용이없는 경우에만 항목을 제거 할 가능성이 높습니다.


답변

전역 잠금을 방지하려면 SingletonCache를 사용하여 메모리 사용량을 늘리지 않고 키당 하나의 잠금을 구현할 수 있습니다 (잠금 개체는 더 이상 참조되지 않을 때 제거되고 획득 / 해제는 스레드로부터 안전하여 비교를 통해 1 개의 인스턴스 만 사용됨을 보장합니다). 스왑).

사용하면 다음과 같습니다.

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        return MemoryCache.Default[CacheKey] as string;
    }

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    {
        lock (lifetime.Value)
        {
           if (MemoryCache.Default.Contains(CacheKey))
           {
              return MemoryCache.Default[CacheKey] as string;
           }

           cacheItemPolicy cip = new CacheItemPolicy()
           {
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           };
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        }
    }
}

코드는 GitHub에 있습니다 : https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

또한 MemoryCache보다 가벼운 LRU 구현이 있으며 더 빠른 동시 읽기 및 쓰기, 제한된 크기, 백그라운드 스레드 없음, 내부 성능 카운터 등 여러 가지 이점이 있습니다 (면책 조항, 내가 작성했습니다).


답변

콘솔 예제MemoryCache , “저장 방법 / 간단한 클래스 개체를 얻을”

출력 시작하고 누른 후 Any key제외Esc :

캐시에 저장 중!
캐시에서 가져 오기!
Some1
Some2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }


답변

public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}