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

问题描述

| 场景: 我有一堆子对象,都与给定的父对象有关。 我正在使用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,所以我认为我需要一个读锁。