方案:
List<T>
实现时不是线程安全的。我已阅读有关ConcurrentBag<T>
和ConcurrentDictionary<T>
但未确定是否应使用其中之一,或自行实施同步/锁定。
我在.NET 4上,所以我使用ObjectCache
作为MemoryCache.Default
的单例实例。我有一个与缓存一起工作的服务层,服务接受ObjectCache
的实例,这是通过构造函数DI完成的。这样,所有服务共享相同的ObjectCache
实例。
主线程安全问题是我需要遍历当前的“缓存”集合,如果我正在使用的孩子已经在那里,我需要删除它并添加我正在使用的那个,这个枚举是导致问题的原因。
有什么建议吗?
答案 0 :(得分:8)
是的,要有效地实现缓存,它需要快速查找机制,因此List<T>
是开箱即用的错误数据结构。 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
来增强信心并添加一些编译时安全检查。
但重要的是,List<T>
只有在可变的情况下才是线程安全的。因为根据定义,使用缓存实现的方法如果多次调用必须返回相同的值(直到缓存本身使值无效),客户端无法修改返回的值,因此必须冻结List<T>
不可变的。
如果客户端迫切需要修改缓存的数据库结果且值为List<T>
,那么唯一安全的方法是:
总之,对顶级缓存使用线程安全字典,对缓存值使用普通列表,注意在将其插入缓存后永远不要修改最后一个内容。
答案 1 :(得分:2)
尝试修改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;
}
答案 2 :(得分:1)
编辑 - 更新为使用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;
}
}
<强>要点:强>
ReaderWriterLockSlim
来确保多个线程可以读取,但只有一个线程可以写。我希望这应该避免“读”死锁。如果两个线程碰巧需要同时“写入”,那么当然必须等待。但这里的目标是快速提供阅读。此方法的任何提示/建议/问题?我不确定我是否锁定写区域。但是因为我需要在字典上运行LINQ,我想我需要一个读锁定。