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.