问题描述
|
场景:
我有一堆子对象,都与给定的父对象有关。
我正在使用ASP.NET MVC 3 Web应用程序(例如多线程)
其中一个页面是“搜索”页面,我需要在其中抓取给定子集的子集并向他们的内存中“做东西”(计算,排序,枚举)
我没有对每个孩子进行单独的调用,而是对数据库进行了一次调用,以获取给定父级的所有孩子,对结果进行缓存并在结果上“做一些事情”。
问题是\ do dos \涉及LINQ操作(枚举,从集合中添加/删除项目),当使用
List<T>
实现时,该操作不是线程安全的。
我已经读过关于ConcurrentBag<T>
和ConcurrentDictionary<T>
的信息,但是不确定是否应该使用其中之一,或者自己实现同步/锁定。
我在.NET 4上,因此我将ObjectCache
用作MemoryCache.Default
的单例实例。我有一个与缓存配合使用的服务层,并且服务接受an3ѭ的实例,这是通过构造函数DI完成的。这样,所有服务共享相同的“ 3”实例。
主要的线程安全问题是我需要遍历当前的\“ cached \”集合,如果我正在使用的子代已经存在,则需要删除它并添加一个正在使用的子代。在这种情况下,是造成问题的原因。
有什么建议吗?
解决方法
是的,要有效地实现高速缓存,它需要一种快速的查找机制,因此“ 0”是开箱即用的错误数据结构。
Dictionary<TKey,TValue>
是高速缓存的理想数据结构,因为它提供了一种替换方法:
var value = instance.GetValueExpensive(key);
与:
var value = instance.GetValueCached(key);
通过在字典中使用缓存的值并使用字典进行繁重的查找工作。呼叫者都不是明智的。
但是,如果调用者可以从多个线程进行调用,则.NET4提供了ConcurrentDictionary<TKey,TValue>
,在这种情况下可以很好地工作。但是字典缓存什么?在您的情况下,字典键似乎是该子项,而字典值是该子项的数据库结果。
好的,现在我们有了一个由子键控的线程安全高效的数据库结果缓存。我们应该为数据库结果使用什么数据结构?
您没有说出这些结果是什么样子,但是由于您使用LINQ,我们知道它们至少是IEnumerable<T>
甚至是List<T>
。所以我们又回到了同样的问题,对吗?由于List<T>
不是线程安全的,因此我们不能将其用作字典值。还是可以?
从调用者的角度来看,缓存必须是只读的。您说使用LINQ,您可以像添加/删除一样“执行操作”,但这对于缓存的值没有意义。仅在缓存本身的实现中做些事情(例如用新结果替换陈旧的条目)才有意义。
由于字典值是只读的,因此它可以是一个没有不良影响的List<T>
,即使可以从多个线程访问它也是如此。您可以使用List<T>.AsReadOnly
增强信心,并添加一些编译时安全检查。
但是重要的一点是,“ 0”仅是可变的,它不是线程安全的。根据定义,由于使用高速缓存实现的方法必须多次调用才能返回相同的值(直到该值本身被高速缓存本身无效),因此客户端无法修改返回的值,因此必须将0冻结,使其有效地保持不变。
如果客户端急需修改缓存的数据库结果,并且该值为List<T>
,则唯一安全的方法是:
制作副本
更改副本
要求缓存更新值
总而言之,对顶级缓存使用线程安全的字典,对缓存的值使用普通列表,请小心,切勿在将最后一个的内容插入缓存后对其进行修改。
,尝试像这样修改RefreshFooCache:
public ReadOnlyCollection<Foo> RefreshFooCache(Foo parentFoo)
{
ReadOnlyCollection<Foo> results;
try {
// Prevent other writers but allow read operations to proceed
Lock.EnterUpgradableReaderLock();
// Recheck your cache: it may have already been updated by a previous thread before we gained exclusive lock
if (_cache.Get(parentFoo.Id.ToString()) != null) {
return parentFoo.FindFoosUnderneath(uniqueUri).AsReadOnly();
}
// Get the parent and everything below.
var parentFooIncludingBelow = _repo.FindFoosIncludingBelow(parentFoo.UniqueUri).ToList();
// Now prevent both other writers and other readers
Lock.EnterWriteLock();
// Remove the cache
_cache.Remove(parentFoo.Id.ToString());
// Add the cache.
_cache.Add(parentFoo.Id.ToString(),parentFooIncludingBelow );
} finally {
if (Lock.IsWriteLockHeld) {
Lock.ExitWriteLock();
}
Lock.ExitUpgradableReaderLock();
}
results = parentFooIncludingBelow.AsReadOnly();
return results;
}
,编辑-更新为使用ReadOnlyCollection而不是ConcurrentDictionary
这是我目前已实现的功能。我进行了一些负载测试,没有看到任何错误发生-你们如何看待这种实现:
public class FooService
{
private static ReaderWriterLockSlim _lock;
private static readonly object SyncLock = new object();
private static ReaderWriterLockSlim Lock
{
get
{
if (_lock == null)
{
lock(SyncLock)
{
if (_lock == null)
_lock = new ReaderWriterLockSlim();
}
}
return _lock;
}
}
public ReadOnlyCollection<Foo> RefreshFooCache(Foo parentFoo)
{
// Get the parent and everything below.
var parentFooIncludingBelow = repo.FindFoosIncludingBelow(parentFoo.UniqueUri).ToList().AsReadOnly();
try
{
Lock.EnterWriteLock();
// Remove the cache
_cache.Remove(parentFoo.Id.ToString());
// Add the cache.
_cache.Add(parentFoo.Id.ToString(),parentFooIncludingBelow);
}
finally
{
Lock.ExitWriteLock();
}
return parentFooIncludingBelow;
}
public ReadOnlyCollection<Foo> FindFoo(string uniqueUri)
{
var parentIdForFoo = uniqueUri.GetParentId();
ReadOnlyCollection<Foo> results;
try
{
Lock.EnterReadLock();
var cachedFoo = _cache.Get(parentIdForFoo);
if (cachedFoo != null)
results = cachedFoo.FindFoosUnderneath(uniqueUri).ToList().AsReadOnly();
}
finally
{
Lock.ExitReadLock();
}
if (results == null)
results = RefreshFooCache(parentFoo).FindFoosUnderneath(uniqueUri).ToList().AsReadOnly();
}
return results;
}
}
摘要:
每个控制器(例如每个HTTP请求)都获得了FooService的新实例。
FooService具有静态锁,因此所有HTTP请求共享同一实例。
我使用ѭ22来确保可以读取多个线程,但是只能写入一个线程。我希望这应该避免“读取”死锁。如果两个线程恰巧需要同时“写入”,那么当然必须等待。但这里的目标是快速提供阅读服务。
目的是能够从高速缓存中找到\“ Foo \”,只要已检索到上方的内容。
此方法有任何提示/建议/问题吗?我不确定即时通讯是否锁定写入区域。但是因为我需要在字典上运行LINQ,所以我认为我需要一个读锁。