2011-05-25 28 views
3

方案如何在使用集合时实现线程安全缓存机制?

  • 我有一堆儿童对象,凡是涉及到一个给定的家长。我正在处理一个ASP.NET MVC 3 Web应用程序(例如多线程)
  • 其中一个网页是一个“搜索”页面,我需要抓住一个给定的儿童子集和“做东西“(计算,订购,枚举)
  • 而不是让每个孩子在一个单独的调用,我做一个调用数据库来获取给定的父母的所有孩子,缓存结果和”做的东西“结果。
  • 问题是,“做什么”涉及LINQ操作(枚举,添加/删除集合中的项目),当使用List<T>实施时,它不是线程安全的。

我看了一下ConcurrentBag<T>ConcurrentDictionary<T>,但不知道我是否应该用其中的一个,或实现同步/锁定自己。

我在.NET 4上,所以我利用ObjectCache作为MemoryCache.Default的单例实例。我有一个与缓存一起工作的服务层,服务接受通过构造函数DI完成的ObjectCache实例。这样所有的服务共享相同的ObjectCache实例。

主线程安全问题是我需要遍历当前的“缓存”集合,如果我正在使用的孩子已经在那里,我需要删除它并添加一个我正在使用的在这个枚举是导致问题的原因。

有什么建议吗?

回答

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>,那么这样做是对的唯一安全的方法:

  1. 制作副本
  2. 进行更改,拷贝
  3. 问cache来更新值

总之,使用一个线程安全的字典顶级缓存和缓存值的普通列表中,请小心NE ver将其插入缓存后修改最后一个内容。

+0

@Rick - 感谢您的回答,真的有见地+1。你可以看看我发布的答案,看看我是否在正确的轨道上。我真的不想做一个深层的复制品,因为这些藏品非常大。我用不同的方法完成了 - 看一看。 – RPM1984 2011-05-26 23:39:45

+1

@ RPM1984:许多事情看起来不错。但我认为你高估了复制'List '的成本。每次你使用LINQ的'ToList'时,你都会做一个_shallow_副本。这就是我推荐的那种复制。要访问回购,你会自动获得一个全新的'List'。我想你可以调用'AsReadOnly'并将其放在缓存中。我没有看到第二级'ConcurrentDictionary'的优点,但我可能会错过一些东西。 – 2011-05-27 02:01:19

+0

@Rick - 你的权利,并发字典不提供任何价值,因为它在锁内。当从缓存中拉出时,我已更改为'ReadOnlyCollection ',并且在添加到缓存时接受'List '。我想保留RWLS只是为了让人安心,至少。 – RPM1984 2011-05-27 05:07:32

1

编辑 - 更新,而不是使用ConcurrentDictionary

ReadOnlyCollection这是我目前已经实现。我跑了一些负载测试,并没有看到任何错误发生 - 你们怎么想到这个实现的:

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请求共享相同的实例。
  • 我用ReaderWriterLockSlim来确保多线程可以,但只有一个线程可以。我希望这应该避免“读”死锁。如果两个线程需要同时“写”,那么当然需要等待。但这里的目标是快速提供读取。
  • 目标是能够从缓存中找到一个“Foo”,只要它已经被检索到。

此方法的任何提示/建议/问题?我不确定是否锁定了写入区域。但是因为我需要在字典上运行LINQ,我想我需要一个读锁。

+1

三条评论:首先,围绕关键解锁使用'try/finally'块。其次,假设数据库/ repo调用较慢并且缓存查找速度很快,则应该重新检查缓存假设,如下所示: 'try {Lock.EnterWriteLock();如果(_cache.Get(parentFoo.Id.ToString())!= null) 返回parentFoo.FindFoosUnderneath(uniqueUri).AsReadOnly(); } else { // refresh cache } } finally { Lock.ExitWriteLock(); }' 第三,通过将结果=分配移出锁定区域,可以稍微收紧锁定的区域。 – emfurry 2011-05-27 05:58:14

+0

@emfurry - 可能会添加一个答案?很难在评论中阅读代码。 – RPM1984 2011-05-27 06:01:13

+0

哈好点。完成。 – emfurry 2011-05-27 06:07:04

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; 
     } 
相关问题