locking模式以正确使用.NET MemoryCache

我假设这个代码有并发问题:

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; } 

并发问题的原因是多个线程可以获得空密钥,然后尝试将数据插入caching。

什么是最短和最干净的方式来使这个代码并发certificate? 我喜欢在caching相关代码中遵循一个良好的模式。 一个在线文章的链接将是一个很大的帮助。

更新:

我根据@Scott Chamberlain的回答提出了这个代码。 任何人都可以find任何性能或并发问题? 如果这样做,它会节省许多代码和错误。

 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是线程安全的,因此不需要locking初始读取,您可以只读,如果caching返回null,则执行locking检查以查看是否需要创buildstring。 这大大简化了代码。

 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来保护访问。 您需要执行一种“ 双重lockinglocking ”,以查看是否有其他人创build了caching项目,而我们正在等待locking。

 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(); } } 

我已经通过使用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; } 

最糟糕的情况是你创build两个相同的Lazy对象。 但这是相当微不足道的。 AddOrGetExisting的使用保证你只能得到一个Lazy对象的实例,所以你也保证只调用一次昂贵的初始化方法。

有一个开放源代码库[免责声明:我写道]: LazyCache国际海事组织用两行代码覆盖您的要求:

 IAppCache cache = new CachingService(); var cachedResults = cache.GetOrAdd("CacheKey", () => SomeHeavyAndExpensiveCalculation()); 

它默认build立了locking,所以可caching的方法每个caching未命中只执行一次,并且它使用一个lambda,所以你可以一次完成“获取或添加”。 它默认为20分钟滑动到期。

甚至有一个NuGet包 ;)

我假设这个代码有并发问题:

事实上,虽然可能有所改善,但还是可能的。

现在,一般来说,我们有多个线程在第一次使用时设置一个共享的值的模式,不locking正在获取和设置的值可以是:

  1. 灾难性 – 其他代码将假设只有一个实例存在。
  2. 灾难性 – 获取实例的代码不是只能容忍一个(或者可能是某个小数量)并发操作。
  3. 灾难性的 – 存储的手段是不是线程安全的(例如有两个线程添加到字典,你可以得到各种讨厌的错误)。
  4. 次优 – 整体性能比locking确保只有一个线程获得价值的工作更差。
  5. 最佳 – multithreading做冗余工作的成本低于防止成本,特别是因为这只能在相对短的时间内发生。

但是,考虑到MemoryCache可能会驱逐条目,那么:

  1. 如果有多个实例是灾难性的,那么MemoryCache就是错误的方法。
  2. 如果你必须防止同时创build,你应该在创build的时候这样做。
  3. MemoryCache在访问该对象方面是线程安全的,所以这不是一个问题。

当然,这两种可能性都必须考虑,尽pipe只有两个相同string的实例存在才会成为一个问题,如果您正在进行非常特殊的优化,则不适用于此。

所以,我们留下了可能性:

  1. 避免重复调用SomeHeavyAndExpensiveCalculation()的成本会更便宜。
  2. SomeHeavyAndExpensiveCalculation()重复调用的代价是不会更便宜的。

而解决这个问题可能很困难(事实上,这种方法值得剖析,而不是假设你能解决问题)。 在这里值得考虑一下,locking插入的最明显的方法将防止所有的caching添加,包括那些不相关的。

这意味着,如果我们有50个线程试图设置50个不同的值,那么我们将不得不使所有50个线程相互等待,即使他们甚至不计算相同的值。

因此,你可能比使用代码更好,而不是使用避免竞争条件的代码,并且如果竞争条件是一个问题,那么很可能需要在其他地方处理,或者需要一个不同的地方caching策略比驱逐旧条目†。

我会改变的一件事是我将replaceSet()与一个AddOrGetExisting()的调用。 从上面应该可以清楚看出,这可能不是必要的,但它可以让新获得的物品被收集起来,减less了整个内存的使用,并且允许较低的一代到另一代的收集比例更高。

所以是的,你可以使用双重locking来防止并发,但是并发不是真正的问题,或者你的存储方式是错误的,或者在存储上双重locking并不是解决问题的最好方法。

*如果你知道每一个string都存在一个,你可以优化相等的比较,这是关于唯一的一次有两个string的副本可能是错误的,而不是仅仅是次优的,但是你想要做的非常不同types的caching,这是有道理的。 例如XmlReader在内部进行sorting。

†很可能无论是无限期存储还是使用弱引用,所以只有在没有现有用途的情况下才会排除条目。