web-dev-qa-db-ja.com

.NET MemoryCacheを適切に使用するためのロックパターン

このコードには並行性の問題があると思います:

const string CacheKey = "CacheKey";
static string GetCachedData()
{
    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    {
        expensiveString = MemoryCache.Default[CacheKey] as string;
    }
    else
    {
        CacheItemPolicy cip = new CacheItemPolicy()
        {
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        };
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    }
    return expensiveString;
}

同時実行性の問題の理由は、複数のスレッドがnullキーを取得し、キャッシュにデータを挿入しようとする可能性があるためです。

このコードの並行性を証明するための最短で最もクリーンな方法は何でしょうか?キャッシュ関連のコード全体で良いパターンに従うのが好きです。オンライン記事へのリンクは大きな助けになるでしょう。

更新:

@Scott Chamberlainの答えに基づいてこのコードを思いつきました。誰でもこれでパフォーマンスや同時実行の問題を見つけることができますか?これが機能する場合、多くのコード行とエラーを節約できます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc
{
    class Program
    {
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        {
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        }

        private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
        private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}

        public static class MemoryCacheHelper
        {
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            {
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                {
                    return cachedData;
                }

                lock (cacheLock)
                {
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    {
                        return cachedData;
                    }

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    {
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    };
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                }
            }
        }
    }
}
101
Allan Xu

これは、コードの2回目の反復です。 MemoryCacheはスレッドセーフなので、最初の読み取りでロックする必要はありません。読み取りだけでキャッシュがnullを返す場合、ロックチェックを実行して文字列を作成する必要があるかどうかを確認できます。コードを大幅に簡素化します。

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()
{

    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    {
        return cachedString;
    }

    lock (cacheLock)
    {
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              {
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              };
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    }
}

EDIT:以下のコードは不要ですが、元の方法を示すために残したかったです。これは、スレッドセーフな読み取りはあるがスレッドセーフでない書き込みを行う別のコレクションを使用している将来の訪問者にとって有用かもしれません(System.Collections名前空間の下のクラスのほとんどはそのようなものです)。

ReaderWriterLockSlimを使用してアクセスを保護する方法を次に示します。何らかの「 Double Checked Locking 」を実行して、ロックを取得するのを待っている間に他の誰かがキャッシュされたアイテムを作成したかどうかを確認する必要があります。

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()
{
    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    {
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }
    }
    finally
    {
        cacheLock.ExitReadLock();
    }

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    {
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        {
            return cachedString;
        }

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        {
            CacheItemPolicy cip = new CacheItemPolicy()
            {
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            };
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        }
        finally 
        {
            cacheLock.ExitWriteLock();
        }
    }
    finally
    {
        cacheLock.ExitUpgradeableReadLock();
    }
}
82

オープンソースライブラリがあります[免責事項:私が書いた]: LazyCache IMOはあなたの要件を2行のコードでカバーします:

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

デフォルトでロックが組み込まれているため、キャッシュ可能なメソッドはキャッシュミスごとに1回だけ実行され、ラムダを使用するため、一度に「取得または追加」を行うことができます。デフォルトでは、スライド式の有効期限は20分です。

NuGetパッケージ ;)もあります

34
alastairtree

MemoryCacheで AddOrGetExisting メソッドを使用し、 Lazy初期化 を使用して、この問題を解決しました。

基本的に、私のコードは次のようになります。

static string GetCachedData(string key, DateTimeOffset offset)
{
    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;
}

ここでの最悪のシナリオは、同じLazyオブジェクトを2回作成することです。しかし、それは非常に簡単です。 AddOrGetExistingを使用すると、Lazyオブジェクトのインスタンスが1つしか取得されないことが保証されるため、高価な初期化メソッドを1回だけ呼び出すことも保証されます。

31
Keith

このコードには並行性の問題があると思います:

実際には、改善される可能性はありますが、おそらく大丈夫です。

現在、一般に、最初の使用時に複数のスレッドが共有値を設定し、取得および設定される値をロックしないパターンは次のとおりです。

  1. 悲惨な-他のコードは、インスタンスが1つだけ存在すると想定します。
  2. 悲惨な-インスタンスを取得するコードは、1つの(またはおそらく特定の少数の)同時操作しか許容できない。
  3. 悲惨-ストレージの手段はスレッドセーフではありません(たとえば、辞書に2つのスレッドを追加すると、あらゆる種類の厄介なエラーが発生する可能性があります)。
  4. 準最適-全体のパフォーマンスは、ロックによって値を取得する作業が1つのスレッドのみで行われた場合よりも低下します。
  5. 最適-複数のスレッドに冗長な作業を行わせるコストは、それを防止するコストよりも低くなります。特に、比較的短い期間にしか発生しないためです。

ただし、ここでMemoryCacheがエントリを削除する可能性があることを考慮してください。

  1. 複数のインスタンスを持つことが悲惨な場合、MemoryCacheは間違ったアプローチです。
  2. 同時作成を防止する必要がある場合は、作成時にそれを行う必要があります。
  3. MemoryCacheは、そのオブジェクトへのアクセスに関してスレッドセーフであるため、ここでは問題になりません。

もちろん、これらの可能性の両方を考慮する必要がありますが、同じ文字列の2つのインスタンスが存在する場合に問題になるのは、ここに当てはまらない非常に特定の最適化を行っている場合だけです*。

したがって、次の可能性が残っています。

  1. SomeHeavyAndExpensiveCalculation()の重複呼び出しのコストを避ける方が安価です。
  2. SomeHeavyAndExpensiveCalculation()の重複呼び出しのコストを回避しない方が安価です。

そして、それを解決するのは難しい場合があります(実際、あなたがそれを解決できると仮定するのではなく、プロファイリングする価値があるようなものです)。ただし、挿入をロックする最も明白な方法は、無関係なものを含め、キャッシュへのallの追加を防ぐことになるので、ここで検討する価値があります。

つまり、50の異なる値を設定しようとする50のスレッドがある場合、同じ計算を実行することさえしなかったとしても、50のスレッドすべてを互いに待機させる必要があります。

そのため、競合状態を回避するコードよりも、お持ちのコードの方がおそらく良いでしょう。競合状態が問題である場合は、別の場所でそれを処理するか、別の古いエントリを追放するものよりもキャッシュ戦略†。

変更する1つのことは、Set()の呼び出しをAddOrGetExisting()の呼び出しに置き換えることです。上記から、おそらく必要ではないことは明らかですが、新しく取得したアイテムを収集できるため、全体的なメモリ使用量が削減され、低世代と高世代のコレクションの比率が高くなります。

そのため、ダブルロックを使用して同時実行を防止できますが、同時実行は実際には問題ではないか、値を間違った方法で保存するか、ストアでのダブルロックはそれを解決する最良の方法ではありません。

*文字列のセットがそれぞれ1つしか存在しないことがわかっている場合、等値比較を最適化できます。これは、文字列のコピーが2つあるだけで次善ではなく間違っている可能性がありますが、意味のある、非常に異なるタイプのキャッシング。例えば。 XmlReaderのソートは内部的に行われます。

†かなり無期限に保存するもの、または弱参照を使用して既存の用途がない場合にのみエントリを追放するもののいずれかです。

15
Jon Hanna
public interface ILazyCacheProvider : IAppCache
{
    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);
}

/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider
{
    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    {
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        {
            _hash[dataKey] = null;
            _logger.Debug($"{dataKey} - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"{dataKey} - first");
        }
        else
        {
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                {
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    {
                        if (ObjectCache.Contains(dataKey))
                        {
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        }
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"{dataKey} - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"{dataKey} - reload");
                        _reloader.Remove(dataKey);
                    }
                });
        }
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"{dataKey} - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"{dataKey} - dummy");
        return (T)((object)data).CloneObject();
    }
}
1
art24war

MemoryCache のコンソール例、「単純なクラスオブジェクトを保存/取得する方法」

起動して押した後の出力 Any key を除く Esc :

キャッシュに保存しています!
キャッシュから取得!
一部1
Some2

    class Some
    {
        public String text { get; set; }

        public Some(String text)
        {
            this.text = text;
        }

        public override string ToString()
        {
            return text;
        }
    }

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    {

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do {

            if (cache.Contains(cache_name))
            {
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            }
            else
            {
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            }

        } while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    }
1
fr0ga