All*_* Xu 110 .net c# multithreading 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;
}
Run Code Online (Sandbox Code Playgroud)
并发问题的原因是多个线程可以获取空键,然后尝试将数据插入缓存.
什么是最简洁,最干净的方法来使这个代码并发证明?我喜欢在缓存相关代码中遵循一个好的模式.链接到在线文章将是一个很大的帮助.
更新:
我根据@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;
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
Sco*_*ain 88
这是我的第二次代码迭代.因为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;
}
}
Run Code Online (Sandbox Code Playgroud)
编辑:下面的代码是不必要的,但我想留下它来显示原始方法.对于使用具有线程安全读取但非线程安全写入的不同集合的未来访问者(对于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();
}
}
Run Code Online (Sandbox Code Playgroud)
ala*_*ree 38
有一个开源库[免责声明:我写的]:LazyCache,IMO通过两行代码满足您的需求:
IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey",
() => SomeHeavyAndExpensiveCalculation());
Run Code Online (Sandbox Code Playgroud)
它默认内置锁定,因此可缓存方法每次缓存未命中时只执行一次,并且它使用lambda,因此您可以一次性"获取或添加".默认为20分钟滑动到期.
甚至还有一个NuGet包 ;)
Kei*_*ith 30
我已经通过在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;
}
Run Code Online (Sandbox Code Playgroud)
这里最糟糕的情况是你创建Lazy
两次相同的对象.但这非常微不足道.使用AddOrGetExisting
保证只能获得Lazy
对象的一个实例,因此您也可以保证只调用一次昂贵的初始化方法.
Jon*_*nna 15
我假设此代码存在并发问题:
实际上,尽管可能有所改善,但它可能很好.
现在,通常我们有多个线程在第一次使用时设置共享值,而不是锁定正在获取和设置的值的模式可以是:
但是,考虑到这里MemoryCache
可能会逐出条目:
MemoryCache
是灾难性的,那么这是错误的方法.MemoryCache
在访问该对象方面是线程安全的,因此这不是一个问题.当然,必须考虑这两种可能性,尽管现有的两个相同字符串实例的唯一时间可能是一个问题,如果你正在做非常特殊的优化,这里不适用*.
所以,我们留下了各种可能性:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.并且解决这个问题可能很困难(事实上,这种事情值得分析,而不是假设你可以解决它).这里值得考虑,但是最明显的锁定插入方式将阻止对缓存的所有添加,包括那些不相关的缓存.
这意味着如果我们有50个线程试图设置50个不同的值,那么我们必须让所有50个线程相互等待,即使它们甚至不会进行相同的计算.
因此,你可能更好地使用你拥有的代码,而不是避免竞争条件的代码,如果竞争条件是一个问题,你很可能需要在其他地方处理,或者需要一个不同的缓存策略比驱逐旧条目的策略†.
我要改变的一件事是我Set()
用一个替换呼叫AddOrGetExisting()
.从上面可以清楚地看出,它可能没有必要,但它将允许收集新获得的项目,减少总体内存使用并允许较低的低代与高代收集率.
所以,是的,您可以使用双锁来防止并发,但要么并发实际上不是问题,要么以错误的方式存储值,或者对商店进行双重锁定将不是解决它的最佳方法.
*如果您只知道一组字符串中的每一个都存在,您可以优化相等比较,这是唯一一次有两个字符串副本可能不正确而不是仅次优,但您想要做的事情有意义的非常不同类型的缓存.例如,排序XmlReader
在内部进行.
†很可能是无限期存储的,或者是使用弱引用的,因此只有在没有现有用途的情况下才会驱逐条目.
归档时间: |
|
查看次数: |
91801 次 |
最近记录: |