이 코드에는 동시성 문제가 있다고 가정합니다.
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
개체의 인스턴스를 하나만 얻을 수 있으므로 값 비싼 초기화 메서드를 한 번만 호출 할 수 있습니다.
답변
이 코드에는 동시성 문제가 있다고 가정합니다.
실제로는 개선이 가능하지만 상당히 괜찮습니다.
이제 일반적으로 획득 및 설정되는 값을 잠그지 않기 위해 처음 사용할 때 공유 값을 설정하는 여러 스레드가있는 패턴은 다음과 같습니다.
- 재앙-다른 코드는 하나의 인스턴스 만 존재한다고 가정합니다.
- 비참함-인스턴스를 얻는 코드는 하나 (또는 특정 소수)의 동시 작업 만 허용 할 수 없습니다.
- 비참한-저장 수단은 스레드로부터 안전하지 않습니다 (예 : 사전에 두 개의 스레드가 추가되면 모든 종류의 불쾌한 오류가 발생할 수 있습니다).
- 차선-잠금으로 인해 하나의 스레드 만 값을 얻는 작업을 수행 한 경우보다 전체 성능이 더 나쁩니다.
- 최적-다중 스레드가 중복 작업을 수행하는 비용은 특히 비교적 짧은 기간 동안 만 발생할 수 있기 때문에이를 방지하는 비용보다 적습니다.
그러나 여기에서 MemoryCache
항목을 제거 할 수 있음을 고려하면 다음과 같습니다.
- 두 개 이상의 인스턴스를 갖는 것이 재앙이라면
MemoryCache
잘못된 접근 방식입니다. - 동시 생성을 방지해야하는 경우 생성 시점에서해야합니다.
MemoryCache
해당 객체에 대한 액세스 측면에서 스레드로부터 안전하므로 여기서는 문제가되지 않습니다.
물론이 두 가지 가능성을 모두 고려해야하지만 동일한 문자열의 두 인스턴스가 존재하는 유일한 경우는 여기에 적용되지 않는 매우 특별한 최적화를 수행하는 경우입니다 *.
따라서 다음과 같은 가능성이 있습니다.
- 중복 호출 비용을 피하는 것이 더 저렴합니다.
SomeHeavyAndExpensiveCalculation()
. - 에 대한 중복 호출 비용을 피하지 않는 것이 더 저렴합니다
SomeHeavyAndExpensiveCalculation()
.
그리고 그것을 해결하는 것은 어려울 수 있습니다 (실제로, 그것을 해결할 수 있다고 가정하는 것보다 프로파일 링 할 가치가있는 일종의 것입니다). 인서트 잠금의 가장 명백한 방법이 모든 것을 방지 할 수 있지만 여기서 고려할 가치가 있습니다. 은 관련이없는 것을 포함하여 캐시에 대한 추가를 .
즉, 50 개의 서로 다른 값을 설정하려는 50 개의 스레드가있는 경우 동일한 계산을 수행하지 않더라도 50 개의 스레드가 모두 서로를 대기하도록해야합니다.
따라서 경쟁 조건을 피하는 코드보다 보유한 코드를 사용하는 것이 더 낫습니다. 경쟁 조건이 문제인 경우 다른 곳에서 처리해야하거나 다른 코드가 필요합니다. 오래된 항목을 제거하는 것보다 캐싱 전략 †.
내가 바꾸고 싶은 것은 전화를 Set()
하나로 바꾸는 것입니다.AddOrGetExisting()
. 위에서부터 이것이 필요하지 않을 수도 있지만 새로 얻은 항목을 수집하여 전체 메모리 사용을 줄이고 저 세대 대 고 세대 컬렉션의 비율을 높일 수 있다는 것이 분명합니다.
예, 동시성을 방지하기 위해 이중 잠금을 사용할 수 있지만 동시성이 실제로 문제가되지 않거나 값을 잘못된 방식으로 저장하거나 저장소에 이중 잠금을 사용하는 것이 문제를 해결하는 가장 좋은 방법이 아닙니다. .
* 각 문자열 세트가 하나만 존재한다는 것을 알고 있다면 동등성 비교를 최적화 할 수 있습니다. 이는 문자열의 사본이 두 개있는 유일한 시간이 차선책이 아닌 부정확 할 수 있지만 그렇게하고 싶을 것입니다. 매우 다른 유형의 캐싱이 의미가 있습니다. 예를 들어 정렬 XmlReader
은 내부적으로 수행됩니다.
† 무기한 저장하거나 약한 참조를 사용하여 기존 사용이없는 경우에만 항목을 제거 할 가능성이 높습니다.
답변
전역 잠금을 방지하려면 SingletonCache를 사용하여 메모리 사용량을 늘리지 않고 키당 하나의 잠금을 구현할 수 있습니다 (잠금 개체는 더 이상 참조되지 않을 때 제거되고 획득 / 해제는 스레드로부터 안전하여 비교를 통해 1 개의 인스턴스 만 사용됨을 보장합니다). 스왑).
사용하면 다음과 같습니다.
SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();
const string CacheKey = "CacheKey";
static string GetCachedData()
{
string expensiveString =null;
if (MemoryCache.Default.Contains(CacheKey))
{
return MemoryCache.Default[CacheKey] as string;
}
// double checked lock
using (var lifetime = keyLocks.Acquire(url))
{
lock (lifetime.Value)
{
if (MemoryCache.Default.Contains(CacheKey))
{
return MemoryCache.Default[CacheKey] as string;
}
cacheItemPolicy cip = new CacheItemPolicy()
{
AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
};
expensiveString = SomeHeavyAndExpensiveCalculation();
MemoryCache.Default.Set(CacheKey, expensiveString, cip);
return expensiveString;
}
}
}
코드는 GitHub에 있습니다 : https://github.com/bitfaster/BitFaster.Caching
Install-Package BitFaster.Caching
또한 MemoryCache보다 가벼운 LRU 구현이 있으며 더 빠른 동시 읽기 및 쓰기, 제한된 크기, 백그라운드 스레드 없음, 내부 성능 카운터 등 여러 가지 이점이 있습니다 (면책 조항, 내가 작성했습니다).
답변
콘솔 예제 의 MemoryCache , “저장 방법 / 간단한 클래스 개체를 얻을”
출력 시작하고 누른 후 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();
}
}