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

时间:2011-05-25 23:44:08

标签: c# asp.net-mvc caching .net-4.0 thread-safety

方案

  • 我有一堆 Child 对象,都与给定的相关。
  • 我正在研究ASP.NET MVC 3 Web应用程序(例如多线程)
  • 其中一个页面是一个“搜索”页面,我需要抓住一个给定的子集,并在内存中“做东西”(计算,排序,枚举)
  • 我没有让每个孩子单独调用,而是对数据库进行一次调用,以获取给定父级的所有子级,缓存结果并对结果“执行操作”。
  • 问题是“do stuff”涉及LINQ操作(枚举,添加/删除集合中的项目),使用List<T>实现时不是线程安全的。

我已阅读有关ConcurrentBag<T>ConcurrentDictionary<T>但未确定是否应使用其中之一,或自行实施同步/锁定。

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

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

有什么建议吗?

3 个答案:

答案 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. 对副本进行更改
  3. 要求缓存更新值
  4. 总之,对顶级缓存使用线程安全字典,对缓存值使用普通列表,注意在将其插入缓存后永远不要修改最后一个内容。

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

<强>要点:

  • 每个 Controller (例如每个HTTP请求)都会获得 FooService 的新实例。
  • FooService 具有静态锁定,因此所有HTTP请求共享同一个实例。
  • 我使用ReaderWriterLockSlim来确保多个线程可以读取,但只有一个线程可以。我希望这应该避免“读”死锁。如果两个线程碰巧需要同时“写入”,那么当然必须等待。但这里的目标是快速提供阅读。
  • 目标是能够从缓存中找到“Foo”,只要检索到它上面的内容。

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