如何使用MemoryCache处理昂贵的构建操作?

Sed*_*glu 58 .net c# memorycache

在ASP.NET MVC项目中,我们有几个数据实例需要大量资源和时间来构建.我们想要缓存它们.

MemoryCache提供一定程度的线程安全性但不足以避免并行运行多个构建代码实例.这是一个例子:

var data = cache["key"];
if(data == null)
{
  data = buildDataUsingGoodAmountOfResources();
  cache["key"] = data;
}
Run Code Online (Sandbox Code Playgroud)

正如您在繁忙的网站上看到的那样,数百个线程可以同时进入if语句,直到构建数据并使构建操作更慢,不必要地消耗服务器资源.

AddOrGetExisting在MemoryCache中有一个原子实现,但它错误地需要"值来设置"而不是"代码来检索要设置的值",我认为这使得给定的方法几乎完全没用.

我们一直在MemoryCache周围使用我们自己的ad-hoc脚手架来实现它,但它需要明确的locks.使用每个条目的锁定对象很麻烦,我们通常会通过共享远离理想的锁定对象来逃避.这让我觉得避免这种惯例的理由可能是故意的.

所以我有两个问题:

  • 不lock建立代码是一种更好的做法吗?(对于一个人来说,这可能已被证明更具响应性,我想知道)

  • 对于这样的锁,实现MemoryCache的每个条目锁定的正确方法是什么?使用key字符串作为锁定对象的强烈冲动在".NET锁定101"中被忽略.

Sed*_*glu 69

我们通过结合来解决这个问题Lazy<T>,AddOrGetExisting以避免完全需要锁定对象.这是一个示例代码(使用无限到期):

public T GetFromCache<T>(string key, Func<T> valueFactory) 
{
    var newValue = new Lazy<T>(valueFactory);
    // the line belows returns existing item or adds the new value if it doesn't exist
    var value = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
    return (value ?? newValue).Value; // Lazy<T> handles the locking itself
}
Run Code Online (Sandbox Code Playgroud)

那不完整.有一些像"异常缓存"的问题所以你必须决定你想要做什么,以防你的valueFactory抛出异常.但是,其中一个优点是能够缓存空值.

  • AddOrGetExisting应该转换为`as Lazy <T>`.否则,好一个! (3认同)
  • 如果key不存在,则调用newValue.Value`(因为`value`将为null).否则调用`value.Value`返回现有值.怎么了?返回值将产生编译错误,因为它们都具有类型"Lazy <T>".你必须为这两个选项返回`.Value`. (2认同)
  • 您可以在`Lazy <T>`构造函数中指定`LazyThreadSafetyMode.PublicationOnly`以避免缓存异常(如果需要). (2认同)

And*_*tan 11

对于条件添加要求,我总是使用ConcurrentDictionary,它有一个重载GetOrAdd方法,如果需要构建对象,它接受一个委托.

ConcurrentDictionary<string, object> _cache = new
  ConcurrenctDictionary<string, object>();

public void GetOrAdd(string key)
{
  return _cache.GetOrAdd(key, (k) => {
    //here 'k' is actually the same as 'key'
    return buildDataUsingGoodAmountOfResources();
  });
}
Run Code Online (Sandbox Code Playgroud)

实际上我几乎总是使用static并发字典.我曾经有一个ReaderWriterLockSlim实例保护的"普通"词典,但是一旦我切换到.Net 4(它只能从那个开始提供),我开始转换我遇到的任何一个.

ConcurrentDictionary至少可以说,表现令人钦佩:)

使用基于年龄的到期语义更新 Naive实现.还应该确保只创建一次单个项目 - 根据@ usr的建议. 再次更新 - 正如@usr所建议的那样 - 简单地使用a Lazy<T>会简单得多 - 你可以在将创建委托添加到并发字典时将其转发给它.我改变了代码,因为实际上我的锁字典无论如何都不会有效.但我真的应该自己想到这一点(虽然在英国过了午夜,但我已经被击败了.任何同情?当然不是.作为一名开发者,我有足够的咖啡因通过我的血管唤醒死者).

我建议IRegisteredObject使用此实现接口,然后使用该HostingEnvironment.RegisterObject方法注册它- 这样做可以在应用程序池关闭/循环时关闭轮询器线程.

public class ConcurrentCache : IDisposable
{
  private readonly ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>> _cache = 
    new ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>>();

  private readonly Thread ExpireThread = new Thread(ExpireMonitor);

  public ConcurrentCache(){
    ExpireThread.Start();
  }

  public void Dispose()
  {
    //yeah, nasty, but this is a 'naive' implementation :)
    ExpireThread.Abort();
  }

  public void ExpireMonitor()
  {
    while(true)
    {
      Thread.Sleep(1000);
      DateTime expireTime = DateTime.Now;
      var toExpire = _cache.Where(kvp => kvp.First != null &&
        kvp.Item1.Value < expireTime).Select(kvp => kvp.Key).ToArray();
      Tuple<string, Lazy<object>> removed;
      object removedLock;
      foreach(var key in toExpire)
      {
        _cache.TryRemove(key, out removed);
      }
    }
  }

  public object CacheOrAdd(string key, Func<string, object> factory, 
    TimeSpan? expiry)
  {
    return _cache.GetOrAdd(key, (k) => { 
      //get or create a new object instance to use 
      //as the lock for the user code
        //here 'k' is actually the same as 'key' 
        return Tuple.Create(
          expiry.HasValue ? DateTime.Now + expiry.Value : (DateTime?)null,
          new Lazy<object>(() => factory(k)));
    }).Item2.Value; 
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 使用Lazy <T>作为值. (2认同)
  • @usr,无法抗拒,抽了一支香烟,并根据你的建议更新了我的手机使用Lazy <T>.我想,从现在开始,我将更多地使用这种模式! (2认同)