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

우선 아래 코드가 스레드로부터 안전하지 않다는 것을 알고 있다는 사실을 알려주십시오 (수정 : 가능할 수 있음). 내가 고생하는 것은 실제로 테스트에서 실패 할 수있는 구현을 찾는 것입니다. 현재 일부 (대부분) 정적 데이터가 캐시되고 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;
    }
}



답변

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

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


답변

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

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

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


답변

다른 사람들이 말했듯이 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());


답변

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

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

다음이 표시됩니다.

^ 스레드 안전

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


답변

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

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

RedisLazyCache

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

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


답변

@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


답변

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

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

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

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