63

ASP.NET MVCプロジェクトには、構築に大量のリソースと時間を必要とするデータのインスタンスがいくつかあります。それらをキャッシュしたいと思います。

MemoryCache一定レベルのスレッドセーフを提供しますが、建築基準法の複数のインスタンスを並行して実行することを回避するには不十分です。次に例を示します。

var data = cache["key"];
if(data == null)
{
  data = buildDataUsingGoodAmountOfResources();
  cache["key"] = data;
}

忙しいウェブサイトでわかるように、データがビルドされるまで何百ものスレッドが同時にifステートメント内に入り、ビルド操作がさらに遅くなり、サーバーリソースを不必要に消費する可能性があります。

MemoryCacheにはアトミックAddOrGetExistingな実装がありますが、「設定する値を取得するコード」ではなく「設定する値」が誤って必要になるため、特定のメソッドがほぼ完全に役に立たなくなると思います。

私たちはMemoryCacheの周りに独自のアドホックなスキャフォールディングを使用してそれを正しくしていますが、明示的なlocksが必要です。エントリごとのロックオブジェクトを使用するのは面倒であり、通常、理想からはほど遠いロックオブジェクトを共有することで回避できます。そのため、そのような慣習を避ける理由は意図的なものである可能性があると思いました。

だから私は2つの質問があります:

  • lockコードを作成しない方が良い方法ですか?(それは、1つに対してより応答性が高いことが証明された可能性があります、私は疑問に思います)

  • このようなロックに対してMemoryCacheのエントリごとのロックを実現する正しい方法は何ですか?ロックオブジェクトとして文字列を使用するという強い衝動keyは、「。NETlocking101」で却下されます。

4

6 に答える 6

74

ロックオブジェクトの必要性を完全に回避するためにと組み合わせること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
}

それは完全ではありません。「例外キャッシング」のような落とし穴があるので、valueFactoryが例外をスローした場合に何をしたいかを決める必要があります。ただし、利点の1つは、null値もキャッシュできることです。

于 2013-04-09T06:38:09.283 に答える
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();
  });
}

実際には、ほとんどの場合、static同時辞書を使用します。以前は「通常の」辞書をReaderWriterLockSlimインスタンスで保護していましたが、.Net 4に切り替えるとすぐに(それ以降のみ使用可能になります)、遭遇した辞書のいずれかを変換し始めました。

ConcurrentDictionaryのパフォーマンスは控えめに言っても立派です:)

年齢のみに基づく有効期限セマンティクスでNaive実装を更新します。また、@ usrの提案に従って、個々のアイテムが1回だけ作成されるようにする必要があります。 @usrが示唆しているように、再度更新しますLazy<T>。aを使用する方がはるかに簡単です。並行ディクショナリに追加するときに、作成デリゲートをそのデリゲートに転送できます。実際には私のロックの辞書はとにかく機能しなかったので、私はコードを変更しました。しかし、私は本当にそれを自分で考えるべきでした(しかし、ここ英国では真夜中過ぎで、私は打ち負かされています。同情はありますか?もちろんそうではありません。開発者である私は、死者を目覚めさせるのに十分なカフェインを静脈に流しています)

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; 
  }
}
于 2012-05-11T22:23:15.197 に答える
2

一番の答えをC#7に取り入れると、これが私の実装であり、任意のソースタイプTから任意のリターンタイプへのストレージを可能にしますTResult

/// <summary>
/// Creates a GetOrRefreshCache function with encapsulated MemoryCache.
/// </summary>
/// <typeparam name="T">The type of inbound objects to cache.</typeparam>
/// <typeparam name="TResult">How the objects will be serialized to cache and returned.</typeparam>
/// <param name="cacheName">The name of the cache.</param>
/// <param name="valueFactory">The factory for storing values.</param>
/// <param name="keyFactory">An optional factory to choose cache keys.</param>
/// <returns>A function to get or refresh from cache.</returns>
public static Func<T, TResult> GetOrRefreshCacheFactory<T, TResult>(string cacheName, Func<T, TResult> valueFactory, Func<T, string> keyFactory = null) {
    var getKey = keyFactory ?? (obj => obj.GetHashCode().ToString());
    var cache = new MemoryCache(cacheName);
    // Thread-safe lazy cache
    TResult getOrRefreshCache(T obj) {
        var key = getKey(obj);
        var newValue = new Lazy<TResult>(() => valueFactory(obj));
        var value = (Lazy<TResult>) cache.AddOrGetExisting(key, newValue, ObjectCache.InfiniteAbsoluteExpiration);
        return (value ?? newValue).Value;
    }
    return getOrRefreshCache;
}

使用法

/// <summary>
/// Get a JSON object from cache or serialize it if it doesn't exist yet.
/// </summary>
private static readonly Func<object, string> GetJson =
    GetOrRefreshCacheFactory<object, string>("json-cache", JsonConvert.SerializeObject);


var json = GetJson(new { foo = "bar", yes = true });
于 2017-07-28T10:37:33.910 に答える
2

LazyとAddOrGetExistingを組み合わせるSedatのソリューションは刺激的です。このソリューションにはパフォーマンスの問題があることを指摘する必要があります。これは、キャッシュのソリューションにとって非常に重要であるように思われます。

AddOrGetExisting()のコードを見ると、AddOrGetExisting()はロックフリーメソッドではないことがわかります。ロックフリーのGet()メソッドと比較すると、MemoryCacheの利点の1つが無駄になります。

最初にGet()を使用し、次にAddOrGetExisting()を使用して、オブジェクトが複数回作成されないようにすることをお勧めします。

public T GetFromCache<T>(string key, Func<T> valueFactory) 
{
    T value = (T)cache.Get(key);
    if (value != null)
    {
        return value;
    }

    var newValue = new Lazy<T>(valueFactory);
    // the line belows returns existing item or adds the new value if it doesn't exist
    var oldValue = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
    return (oldValue ?? newValue).Value; // Lazy<T> handles the locking itself
}
于 2019-07-05T19:31:47.240 に答える
1

これがあなたが考えているように見えるものに従ったデザインです。最初のロックは短時間だけ発生します。data.Valueへの最後の呼び出しも(下で)ロックされますが、クライアントは、2つが同時に同じアイテムを要求している場合にのみブロックします。

public DataType GetData()
{      
  lock(_privateLockingField)
  {
    Lazy<DataType> data = cache["key"] as Lazy<DataType>;
    if(data == null)
    {
      data = new Lazy<DataType>(() => buildDataUsingGoodAmountOfResources();
      cache["key"] = data;
    }
  }

  return data.Value;
}
于 2013-04-08T21:52:09.500 に答える
1

これがMemoryCache拡張メソッドとしての簡単な解決策です。

 public static class MemoryCacheExtensions
 {
     public static T LazyAddOrGetExitingItem<T>(this MemoryCache memoryCache, string key, Func<T> getItemFunc, DateTimeOffset absoluteExpiration)
     {
         var item = new Lazy<T>(
             () => getItemFunc(),
             LazyThreadSafetyMode.PublicationOnly // Do not cache lazy exceptions
         );

         var cachedValue = memoryCache.AddOrGetExisting(key, item, absoluteExpiration) as Lazy<T>;

         return (cachedValue != null) ? cachedValue.Value : item.Value;
     }
 }

そして、使用法の説明としてそれをテストします。

[TestMethod]
[TestCategory("MemoryCacheExtensionsTests"), TestCategory("UnitTests")]
public void MemoryCacheExtensions_LazyAddOrGetExitingItem_Test()
{
    const int expectedValue = 42;
    const int cacheRecordLifetimeInSeconds = 42;

    var key = "lazyMemoryCacheKey";
    var absoluteExpiration = DateTimeOffset.Now.AddSeconds(cacheRecordLifetimeInSeconds);

    var lazyMemoryCache = MemoryCache.Default;

    #region Cache warm up

    var actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
    Assert.AreEqual(expectedValue, actualValue);

    #endregion

    #region Get value from cache

    actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
    Assert.AreEqual(expectedValue, actualValue);

    #endregion
}
于 2017-08-03T16:25:34.967 に答える