Programing

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

crosscheck 2020. 8. 10. 07:41
반응형

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


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

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(). 위에서 볼 때 아마도 필요하지 않을 수도 있지만 새로 얻은 항목을 수집하여 전체 메모리 사용을 줄이고 저 세대 대 고 세대 컬렉션의 비율을 높일 수 있음을 분명히 알 수 있습니다.

So yeah, you could use double-locking to prevent concurrency, but either the concurrency isn't actually a problem, or your storing the values in the wrong way, or double-locking on the store would not be the best way to solve it.

*If you know only one each of a set of strings exists, you can optimise equality comparisons, which is about the only time having two copies of a string can be incorrect rather than just sub-optimal, but you'd want to be doing very different types of caching for that to make sense. E.g. the sort XmlReader does internally.

†Quite likely either one that stores indefinitely, or one that makes use of weak references so it will only expel entries if there are no existing uses.


Console example of MemoryCache, "How to save/get simple class objects"

다음을 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();
    }
}

참고 URL : https://stackoverflow.com/questions/21269170/locking-pattern-for-proper-use-of-net-memorycache

반응형