MemoryCache 스레드 안전성, 잠금이 필요합니까?


91

우선 아래 코드가 스레드로부터 안전하지 않다는 것을 알고 있다는 사실을 알려주십시오 (수정 : 가능할 수 있음). 내가 고생하는 것은 실제로 테스트에서 실패 할 수있는 구현을 찾는 것입니다. 현재 일부 (대부분) 정적 데이터가 캐시되고 SQL 데이터베이스에서 채워지는 대규모 WCF 프로젝트를 리팩토링하고 있습니다. 하루에 한 번 이상 만료되고 "새로 고침"해야하므로 MemoryCache를 사용하고 있습니다.

아래 코드가 스레드로부터 안전하지 않아야한다는 것을 알고 있지만 과부하 상태에서 실패하고 문제를 복잡하게 만들 수는 없습니다. Google 검색은 두 가지 방법으로 구현을 보여줍니다 (잠금이 필요한지 여부에 관계없이 토론과 결합 된 경우와없는 경우).

다중 스레드 환경에서 MemoryCache에 대한 지식이있는 사람이 적절한 위치에 잠 가야하는지 여부를 확실하게 알려 주어 제거 호출 (거의 호출되지 않지만 요구 사항 임)이 검색 / 재 채우기 중에 발생하지 않도록 할 수 있습니다.

public class MemoryCacheService : IMemoryCacheService
{
    private const string PunctuationMapCacheKey = "punctuationMaps";
    private static readonly ObjectCache Cache;
    private readonly IAdoNet _adoNet;

    static MemoryCacheService()
    {
        Cache = MemoryCache.Default;
    }

    public MemoryCacheService(IAdoNet adoNet)
    {
        _adoNet = adoNet;
    }

    public void ClearPunctuationMaps()
    {
        Cache.Remove(PunctuationMapCacheKey);
    }

    public IEnumerable GetPunctuationMaps()
    {
        if (Cache.Contains(PunctuationMapCacheKey))
        {
            return (IEnumerable) Cache.Get(PunctuationMapCacheKey);
        }

        var punctuationMaps = GetPunctuationMappings();

        if (punctuationMaps == null)
        {
            throw new ApplicationException("Unable to retrieve punctuation mappings from the database.");
        }

        if (punctuationMaps.Cast<IPunctuationMapDto>().Any(p => p.UntaggedValue == null || p.TaggedValue == null))
        {
            throw new ApplicationException("Null values detected in Untagged or Tagged punctuation mappings.");
        }

        // Store data in the cache
        var cacheItemPolicy = new CacheItemPolicy
        {
            AbsoluteExpiration = DateTime.Now.AddDays(1.0)
        };

        Cache.AddOrGetExisting(PunctuationMapCacheKey, punctuationMaps, cacheItemPolicy);

        return punctuationMaps;
    }

    //Go oldschool ADO.NET to break the dependency on the entity framework and need to inject the database handler to populate cache
    private IEnumerable GetPunctuationMappings()
    {
        var table = _adoNet.ExecuteSelectCommand("SELECT [id], [TaggedValue],[UntaggedValue] FROM [dbo].[PunctuationMapper]", CommandType.Text);
        if (table != null && table.Rows.Count != 0)
        {
            return AutoMapper.Mapper.DynamicMap<IDataReader, IEnumerable<PunctuationMapDto>>(table.CreateDataReader());
        }

        return null;
    }
}

ObjectCache는 스레드로부터 안전하므로 클래스가 실패 할 수 있다고 생각하지 않습니다. msdn.microsoft.com/en-us/library/… 동시에 데이터베이스로 이동할 수 있지만 필요한 것보다 더 많은 CPU 만 사용합니다.
— the_lotus nov.

1
ObjectCache는 스레드로부터 안전하지만 구현은 그렇지 않을 수 있습니다. 따라서 MemoryCache 질문.
— Haney

답변:


78

기본 MS 제공 MemoryCache은 전적으로 스레드로부터 안전합니다. 파생 된 사용자 지정 구현은 MemoryCache스레드로부터 안전하지 않을 수 있습니다. MemoryCache상자에서 꺼내지 않은 일반 을 사용하는 경우 스레드로부터 안전합니다. 내 오픈 소스 분산 캐싱 솔루션의 소스 코드를 검색하여 사용 방법을 확인합니다 (MemCache.cs).

https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs


2
David, 확인하기 위해 위의 매우 간단한 예제 클래스에서 .Remove ()에 대한 호출은 실제로 다른 스레드가 Get ()을 호출하는 과정에서 스레드로부터 안전합니까? 반사판을 사용하고 더 깊이 파헤쳐 야한다고 생각하지만 많은 상반되는 정보가 있습니다.
— James Legan 2013

12
스레드로부터 안전하지만 경쟁 조건이 발생하기 쉽습니다. 제거하기 전에 Get이 발생하면 Get에서 데이터가 반환됩니다. 제거가 먼저 발생하면 그렇지 않습니다. 이것은 데이터베이스의 더티 읽기와 매우 유사합니다.
— Haney

10
언급할만한 가치가 있습니다 (아래 다른 답변에서 언급 했듯이 ) dotnet 코어 구현은 현재 완전히 스레드로부터 안전 하지 않습니다 . 특히 GetOrCreate방법. 이 문제 GitHub의에 그에
— AmitE

콘솔을 사용하여 스레드를 실행하는 동안 메모리 캐시에서 "메모리 부족"예외가 발생합니까?
— Faseeh Haris 2019

49

MemoryCache는 다른 답변이 지정한 것처럼 실제로 스레드로부터 안전하지만 일반적인 다중 스레딩 문제가 있습니다. 2 개의 스레드 가 동시에 캐시에서 캐시를 시도 Get(또는 확인 Contains)하면 둘 다 캐시를 ​​놓치고 둘 다 생성됩니다. 결과와 둘 다 결과를 캐시에 추가합니다.

종종 이것은 바람직하지 않습니다. 두 번째 스레드는 첫 번째 스레드가 완료 될 때까지 기다렸다가 결과를 두 번 생성하는 대신 결과를 사용해야합니다.

이것이 제가 LazyCache를 작성한 이유 중 하나였습니다 . 이러한 종류의 문제를 해결하는 MemoryCache의 친숙한 래퍼입니다. Nuget 에서도 사용할 수 있습니다 .


"일반적인 다중 스레딩 문제가 있습니다."그렇기 때문에 .NET에서 AddOrGetExisting사용자 지정 논리를 구현하는 대신 Contains. AddOrGetExistingMemoryCache 의 방법 은 원자 적이며 threadsafe입니다. referencesource.microsoft.com/System.Runtime.Caching/R/…
— Alex

1
예 AddOrGetExisting은 스레드로부터 안전합니다. 그러나 캐시에 추가 될 개체에 대한 참조가 이미 있다고 가정합니다. 일반적으로 AddOrGetExisting은 LazyCache가 제공하는 "GetExistingOrGenerateThisAndCacheIt"을 원하지 않습니다.
— alastairtree

예, "당신이 아직 obejct를 가지고 있지 않다면"요점에 동의했습니다
— Alex

17

다른 사람들이 말했듯이 MemoryCache는 실제로 스레드로부터 안전합니다. 그러나 그 안에 저장된 데이터의 스레드 안전성은 전적으로 귀하의 사용에 달려 있습니다.

Reed Copsey의 동시성 및 유형 에 관한 멋진 게시물 을 인용 ConcurrentDictionary<TKey, TValue>합니다. 물론 여기에 적용 할 수 있습니다.

두 스레드가이 [GetOrAdd]를 동시에 호출하면 TValue의 두 인스턴스를 쉽게 구성 할 수 있습니다.

TValue건설 비용이 많이 든다 면 이것이 특히 나쁠 것이라고 상상할 수 있습니다 .

이 문제를 해결하기 위해 Lazy<T>매우 쉽게 활용할 수 있으며, 이는 우연히 건설 비용이 매우 저렴합니다. 이렇게하면 다중 스레드 상황이 발생하는 경우 Lazy<T>(저렴한) 여러 인스턴스 만 빌드 할 수 있습니다.

GetOrAdd() (GetOrCreate() 의 경우 MemoryCache)는 Lazy<T>모든 스레드에 대해 동일하고 단수 를 반환하며 의 "추가"인스턴스 Lazy<T>는 단순히 버려집니다.

는 호출 Lazy<T>될 때까지 아무 작업도하지 않기 때문에 .Value개체의 인스턴스 하나만 생성됩니다.

이제 몇 가지 코드입니다! 아래는 확장 방법입니다.IMemoryCache 위를 구현 . 메소드 매개 변수 SlidingExpiration에 따라 임의로 설정합니다 int seconds. 그러나 이것은 귀하의 필요에 따라 완전히 사용자 정의 할 수 있습니다.

.netcore2.0 앱에만 해당됩니다.

public static T GetOrAdd<T>(this IMemoryCache cache, string key, int seconds, Func<T> factory)
{
    return cache.GetOrCreate<T>(key, entry => new Lazy<T>(() =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return factory.Invoke();
    }).Value);
}

전화하려면 :

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());

이 모든 작업을 비동기 적으로 수행하려면 MSDN 에 대한 그의 기사 에 있는 Stephen Toub의 뛰어난 AsyncLazy<T>구현을 사용하는 것이 좋습니다 . 내장 lazy 이니셜 라이저 와 promise를 결합합니다 .Lazy<T>Task<T>

public class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<T> valueFactory) :
        base(() => Task.Factory.StartNew(valueFactory))
    { }
    public AsyncLazy(Func<Task<T>> taskFactory) :
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
    { }
}   

이제 비동기 버전 GetOrAdd():

public static Task<T> GetOrAddAsync<T>(this IMemoryCache cache, string key, int seconds, Func<Task<T>> taskFactory)
{
    return cache.GetOrCreateAsync<T>(key, async entry => await new AsyncLazy<T>(async () =>
    { 
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return await taskFactory.Invoke();
    }).Value);
}

마지막으로 다음을 호출합니다.

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());

2
나는 그것을 시도했지만 작동하지 않는 것 같습니다 (dot net core 2.0). 각 GetOrCreate는 새로운 Lazy 인스턴스를 생성하고 캐시는 새로운 Lazy로 업데이트되므로 Value get은 여러 번 평가 / 생성됩니다 (다중 스레드 환경에서).
— AmitE

2
그것의 모습에서, 2.0 .netcore MemoryCache.GetOrCreate같은 방법으로 스레드로부터 안전하지 않습니다 ConcurrentDictionary이다
— AmitE

아마도 어리석은 질문 일 것입니다. 그러나 당신은 그것이 여러 번 생성 된 것이 아니라 Lazy? 그렇다면이를 어떻게 검증 했습니까?
— pim

3
나는 그것이 사용되었을 때 화면에 인쇄하고 난수를 생성하는 공장 기능을 GetOrCreate사용하고 동일한 키와 그 공장 을 사용 하려고 시도하는 10 개의 스레드를 시작했습니다 . 그 결과, 공장은 메모리 캐시와 함께 사용할 때 10 번 사용되었습니다 (인쇄물을 봤습니다) + 매번 GetOrCreate다른 값을 반환했습니다! 나는 동일한 테스트를 사용하여 ConcurrentDicionary한 번만 사용되는 공장을 보았고 항상 동일한 값을 얻었습니다. github에서 닫힌 문제 를 발견했습니다. 방금 다시 열어야한다는 의견을 썼습니다
— AmitE

경고 :이 대답은 작동하지 않습니다. Lazy <T>는 캐시 된 함수의 여러 실행을 방해하지 않습니다. @snicker 답변을 참조하십시오. [net core 2.0]
— Fernando Silva

11

이 링크를 확인하십시오 : http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

페이지 맨 아래로 이동하거나 "스레드 안전"텍스트를 검색하십시오.

다음이 표시됩니다.

^ 스레드 안전

이 유형은 스레드로부터 안전합니다.


7
나는 개인적인 경험을 바탕으로 꽤 오래 전에 MSDN의 "Thread Safe"에 대한 정의를 신뢰하지 않았습니다. 여기에 좋은 읽기는 다음 링크
— 제임스 Legan

3
해당 게시물은 위에서 제공 한 링크와 약간 다릅니다. 이 구분은 내가 제공 한 링크가 스레드 안전성 선언에 대한 경고를 제공하지 않았기 때문에 매우 중요합니다. 또한 MemoryCache.Default아직 스레딩 문제없이 매우 많은 양 (분당 수백만 개의 캐시 히트)을 사용 하는 개인적인 경험이 있습니다 .
— EkoostikMartin 2013

나는 그들이 의미하는 바는 읽기 및 쓰기 작업이 원자 적으로 발생한다는 것입니다. 간단히, 스레드 A가 현재 값을 읽으려고 시도하는 동안 항상 완료된 쓰기 작업을 읽습니다. 스레드가 데이터를 메모리에 쓰는 도중이 아닙니다. 메모리에 쓰기 위해 어떤 스레드로도 개입 할 수 없습니다. 그것은 내 이해이지만 다음과 같은 완전한 결론으로 ​​이어지지 않는 많은 질문 / 기사가 있습니다. stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
— erhan355

3

.Net 2.0 문제를 해결하기 위해 샘플 라이브러리를 업로드했습니다.

이 저장소를 살펴보십시오.

RedisLazyCache

Redis 캐시를 사용하고 있지만 Connectionstring이없는 경우 장애 조치 또는 Memorycache도 사용합니다.

LazyCache 라이브러리를 기반으로 콜백을 실행하는 데 매우 비용이 많이 드는 경우 특별히 데이터를로드하고 저장하려는 다중 스레딩 이벤트에서 쓰기위한 콜백의 단일 실행을 보장합니다.


답변 만 공유하세요. 다른 정보는 댓글로 공유 할 수 있습니다.
— ElasticCode

1
@WaelAbbas. 시도했지만 먼저 50 개의 평판이 필요한 것 같습니다. :디. OP 질문에 대한 직접적인 대답은 아니지만 (예 / 아니오로 대답 할 수 있으며 이유에 대한 설명이 있습니다), 제 대답은 아니오 대답에 대한 가능한 해결책입니다.
— Francis Marasigan

1

@pimbrouwers의 답변에서 @AmitE가 언급했듯이 그의 예제는 여기에 설명 된대로 작동하지 않습니다.

class Program
{
    static async Task Main(string[] args)
    {
        var cache = new MemoryCache(new MemoryCacheOptions());

        var tasks = new List<Task>();
        var counter = 0;

        for (int i = 0; i < 10; i++)
        {
            var loc = i;
            tasks.Add(Task.Run(() =>
            {
                var x = GetOrAdd(cache, "test", TimeSpan.FromMinutes(1), () => Interlocked.Increment(ref counter));
                Console.WriteLine($"Interation {loc} got {x}");
            }));
        }

        await Task.WhenAll(tasks);
        Console.WriteLine("Total value creations: " + counter);
        Console.ReadKey();
    }

    public static T GetOrAdd<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory)
    {
        return cache.GetOrCreate(key, entry =>
        {
            entry.SetSlidingExpiration(expiration);
            return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
        }).Value;
    }
}

산출:

Interation 6 got 8
Interation 7 got 6
Interation 2 got 3
Interation 3 got 2
Interation 4 got 10
Interation 8 got 9
Interation 5 got 4
Interation 9 got 1
Interation 1 got 5
Interation 0 got 7
Total value creations: 10

GetOrCreate항상 생성 된 항목을 반환하는 것 같습니다 . 다행히도 매우 쉽게 수정할 수 있습니다.

public static T GetOrSetValueSafe<T>(IMemoryCache cache, string key, TimeSpan expiration,
    Func<T> valueFactory)
{
    if (cache.TryGetValue(key, out Lazy<T> cachedValue))
        return cachedValue.Value;

    cache.GetOrCreate(key, entry =>
    {
        entry.SetSlidingExpiration(expiration);
        return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
    });

    return cache.Get<Lazy<T>>(key).Value;
}

예상대로 작동합니다.

Interation 4 got 1
Interation 9 got 1
Interation 1 got 1
Interation 8 got 1
Interation 0 got 1
Interation 6 got 1
Interation 7 got 1
Interation 2 got 1
Interation 5 got 1
Interation 3 got 1
Total value creations: 1

2
이것도 작동하지 않습니다. 그냥 여러 번 시도하고 당신은 때때로 값은 항상 1없는 볼 수 있습니다
— mlessard

1

캐시는 스레드로부터 안전하지만 다른 사람들이 언급했듯이 여러 유형에서 호출하면 GetOrAdd가 func 다중 유형을 호출 할 수 있습니다.

여기에 대한 최소한의 수정 사항이 있습니다.

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

과

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();

나는 그것의 좋은 해결책이라고 생각하지만, 다른 캐시를 변경하는 여러 방법이 있으면 잠금을 사용하면 필요없이 잠길 것입니다! 이 경우 여러 개의 _cacheLock이 있어야합니다. 캐시 락에도 Key가있을 수 있다면 더 좋을 것 같습니다!
— Daniel

이 문제를 해결하는 방법에는 여러 가지가 있습니다. 하나는 세마포어가 MyCache <T>의 인스턴스별로 고유 한 제네릭입니다. 그런 다음 등록 할 수 있습니다. AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
— Anders

다른 임시 유형을 호출해야하는 경우 문제를 일으킬 수있는 단일 캐시를 전체 캐시로 만들지는 않을 것입니다. 그래서 아마도 싱글 톤 인 세마포어 저장소 ICacheLock <T>를 가질 수 있습니다
— Anders

이 버전의 문제점은 동시에 캐시 할 두 가지 다른 항목이있는 경우 두 번째 캐시를 확인하기 전에 첫 번째 항목 생성이 완료 될 때까지 기다려야한다는 것입니다. 키가 다른 경우 둘 다 동시에 캐시를 확인하고 생성 할 수있는 것이 더 효율적입니다. LazyCache는 Lazy <T> 및 스트라이프 잠금의 조합을 사용하여 항목을 최대한 빠르게 캐시하고 키당 한 번만 생성합니다. github.com/alastairtree/lazycache
— alastairtree
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.