[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 문제를 해결하기 위해 샘플 라이브러리를 업로드했습니다.
이 저장소를 살펴보십시오.
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();