MemoryCacheスレッドセーフ、ロックは必要ですか?


91

手始めに、以下のコードはスレッドセーフではないことがわかっているので、それを捨てさせてください(修正:そうかもしれません)。私が苦労しているのは、テストで実際に失敗する可能性のある実装を見つけることです。現在、大規模なWCFプロジェクトをリファクタリングしています。このプロジェクトでは、(ほとんどの場合)静的データをキャッシュし、SQLデータベースからデータを取り込む必要があります。少なくとも1日に1回は有効期限が切れて「更新」する必要があるため、MemoryCacheを使用しています。

以下のコードはスレッドセーフであってはならないことは知っていますが、高負荷で失敗することはなく、問題を複雑にするために、グーグル検索は両方の方法で実装を示しています(ロックの有無と必要かどうかの議論が組み合わされています。

マルチスレッド環境でのMemoryCacheの知識がある人は、削除の呼び出し(めったに呼び出されませんが、その要件)が取得/再入力中にスローされないように、必要に応じてロックする必要があるかどうかを明確に教えてくれますか?

public class MemoryCacheService : IMemoryCacheService
{
    private const string PunctuationMapCacheKey = "punctuationMaps";
    private static readonly ObjectCache Cache;
    private readonly IAdoNet _adoNet;

    static MemoryCacheService()
    {
        Cache = MemoryCache.Default;
    }

    public MemoryCacheService(IAdoNet adoNet)
    {
        _adoNet = adoNet;
    }

    public void ClearPunctuationMaps()
    {
        Cache.Remove(PunctuationMapCacheKey);
    }

    public IEnumerable GetPunctuationMaps()
    {
        if (Cache.Contains(PunctuationMapCacheKey))
        {
            return (IEnumerable) Cache.Get(PunctuationMapCacheKey);
        }

        var punctuationMaps = GetPunctuationMappings();

        if (punctuationMaps == null)
        {
            throw new ApplicationException("Unable to retrieve punctuation mappings from the database.");
        }

        if (punctuationMaps.Cast<IPunctuationMapDto>().Any(p => p.UntaggedValue == null || p.TaggedValue == null))
        {
            throw new ApplicationException("Null values detected in Untagged or Tagged punctuation mappings.");
        }

        // Store data in the cache
        var cacheItemPolicy = new CacheItemPolicy
        {
            AbsoluteExpiration = DateTime.Now.AddDays(1.0)
        };

        Cache.AddOrGetExisting(PunctuationMapCacheKey, punctuationMaps, cacheItemPolicy);

        return punctuationMaps;
    }

    //Go oldschool ADO.NET to break the dependency on the entity framework and need to inject the database handler to populate cache
    private IEnumerable GetPunctuationMappings()
    {
        var table = _adoNet.ExecuteSelectCommand("SELECT [id], [TaggedValue],[UntaggedValue] FROM [dbo].[PunctuationMapper]", CommandType.Text);
        if (table != null && table.Rows.Count != 0)
        {
            return AutoMapper.Mapper.DynamicMap<IDataReader, IEnumerable<PunctuationMapDto>>(table.CreateDataReader());
        }

        return null;
    }
}

ObjectCacheはスレッドセーフです。クラスが失敗することはないと思います。msdn.microsoft.com/en-us/library/…同時にデータベースにアクセスする可能性がありますが、必要以上のCPUしか使用しません。
the_lotus 2013年

1
ObjectCacheはスレッドセーフですが、その実装はそうではない場合があります。したがって、MemoryCacheの質問。
ヘイニー

回答:


78

デフォルトのMS提供MemoryCacheは、完全にスレッドセーフです。から派生したカスタム実装は、MemoryCacheスレッドセーフではない可能性があります。MemoryCache箱から出してプレーンを使用している場合は、スレッドセーフです。オープンソースの分散キャッシングソリューションのソースコードを参照して、その使用方法を確認します(MemCache.cs)。

https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs


2
David、確認のために、上記の非常に単純なサンプルクラスでは、別のスレッドがGet()を呼び出すプロセスにある場合、.Remove()の呼び出しは実際にはスレッドセーフですか?リフレクターを使ってもっと深く掘り下げればいいと思いますが、矛盾する情報がたくさんあります。
James Legan 2013年

12
スレッドセーフですが、競合状態になりがちです...削除の前にGetが発生した場合、データはGetで返されます。削除が最初に発生した場合、発生しません。これは、データベースでのダーティリードによく似ています。
ヘイニー

10
言及する価値があります(以下の別の回答でコメントしたように)、dotnetコアの実装は現在完全にスレッドセーフではありません。具体的にはGetOrCreateメソッド。githubに問題があります
AmitE 2018年

コンソールを使用してスレッドを実行しているときに、メモリキャッシュが例外「メモリ不足」をスローしますか?
FaseehHaris19年

49

他の回答が指定しているように、MemoryCacheは確かにスレッドセーフですが、一般的なマルチスレッドの問題があります-2つのスレッドが同時にキャッシュGetから取得(またはチェックContains)しようとすると、両方がキャッシュを見逃し、両方が生成されます結果と両方が結果をキャッシュに追加します。

多くの場合、これは望ましくありません。2番目のスレッドは、結果を2回生成するのではなく、最初のスレッドが完了するのを待ってその結果を使用する必要があります。

これが、私がLazyCacheを作成した理由の1つです。これは、この種の問題を解決するMemoryCacheの使いやすいラッパーです。Nugetでも利用できます。


「一般的なマルチスレッドの問題があります」そのためAddOrGetExisting、の周りにカスタムロジックを実装するのではなく、のようなアトミックメソッドを使用する必要がありますContainsAddOrGetExistingMemoryCacheの方法は、原子で、スレッドセーフ referencesource.microsoft.com/System.Runtime.Caching/R/...
アレックス

1
はいAddOrGetExistingはスレッドセーフです。ただし、キャッシュに追加されるオブジェクトへの参照がすでにあることを前提としています。通常、AddOrGetExistingは必要ありませんが、LazyCacheが提供する「GetExistingOrGenerateThisAndCacheIt」が必要です。
alastairtree

はい、「まだオブジェクトを持っていない場合」のポイントに同意しました
Alex

17

他の人が述べているように、MemoryCacheは確かにスレッドセーフです。ただし、その中に格納されているデータのスレッドセーフは、完全に使用者次第です。

並行性とタイプに関する彼の素晴らしい投稿からReedCopseyを引用します。もちろん、これはここに当てはまります。ConcurrentDictionary<TKey, TValue>

2つのスレッドがこれを同時に[GetOrAdd]と呼ぶ場合、TValueの2つのインスタンスを簡単に構築できます。

TValue構築に費用がかかる場合、これは特に悪いことになると想像できます。

これを回避するためLazy<T>に、非常に簡単に活用できます。これは偶然にも非常に安価に構築できます。これを行うことで、マルチスレッドの状況に陥った場合に、Lazy<T>(安価な)の複数のインスタンスのみを構築していることが保証されます。

GetOrAdd()GetOrCreate()の場合MemoryCache)は、Lazy<T>すべてのスレッドに同じ、特異なものを返します。の「余分な」インスタンスLazy<T>は単に破棄されます。

が呼び出されるLazy<T>まで.Valueは何も実行しないため、オブジェクトのインスタンスは1つだけ作成されます。

今、いくつかのコードのために!以下はIMemoryCache、上記を実装する拡張メソッドです。メソッドパラメータにSlidingExpiration基づいて任意に設定していint secondsます。しかし、これはあなたのニーズに基づいて完全にカスタマイズ可能です。

これは.netcore2.0アプリに固有であることに注意してください

public static T GetOrAdd<T>(this IMemoryCache cache, string key, int seconds, Func<T> factory)
{
    return cache.GetOrCreate<T>(key, entry => new Lazy<T>(() =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return factory.Invoke();
    }).Value);
}

電話するには:

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());

これをすべて非同期で実行するには、MSDNの記事にあるStephenToubの優れたAsyncLazy<T>実装を使用することをお勧めします。これは、組み込みの遅延初期化子とpromiseを組み合わせたものです。Lazy<T>Task<T>

public class AsyncLazy<T> : Lazy<Task<T>>
{
    public AsyncLazy(Func<T> valueFactory) :
        base(() => Task.Factory.StartNew(valueFactory))
    { }
    public AsyncLazy(Func<Task<T>> taskFactory) :
        base(() => Task.Factory.StartNew(() => taskFactory()).Unwrap())
    { }
}   

今の非同期バージョンGetOrAdd()

public static Task<T> GetOrAddAsync<T>(this IMemoryCache cache, string key, int seconds, Func<Task<T>> taskFactory)
{
    return cache.GetOrCreateAsync<T>(key, async entry => await new AsyncLazy<T>(async () =>
    { 
        entry.SlidingExpiration = TimeSpan.FromSeconds(seconds);

        return await taskFactory.Invoke();
    }).Value);
}

そして最後に、電話する:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());

2
試してみましたが、うまくいかないようです(ドットネットコア2.0)。各GetOrCreateは新しいLazyインスタンスを作成し、キャッシュは新しいLazyで更新されるため、Value getは複数回(複数スレッド環境で)評価/作成されます。
AmitE 2018年

2
その様子から、2.0を.netcoreMemoryCache.GetOrCreate同じようにスレッドセーフではありませんConcurrentDictionaryです
アミテ

おそらくばかげた質問ですが、それは複数回作成された値であり、Lazy?もしそうなら、どのようにこれを検証しましたか?
pim 2018年

3
使用時に画面に出力するファクトリ関数を使用して+乱数を生成し、すべてGetOrCreate同じキーとそのファクトリを使用しようとして10個のスレッドを開始しました。その結果、ファクトリはメモリキャッシュで使用するときに10回使用されました(プリントを見ました)+毎回GetOrCreate異なる値が返されました!を使用して同じテストを実行しConcurrentDicionary、ファクトリが1回だけ使用されていることを確認し、常に同じ値を取得しました。githubでクローズされた問題を見つけました。そこで、再度オープンする必要があるというコメントを書きました
AmitE 2018年

警告:この回答は機能していません:Lazy <T>はキャッシュされた関数の複数の実行を妨げていません。@snickerの回答を参照してください。[ネットコア2.0]
フェルナンドシルバ

11

このリンクを確認してください:http//msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v = vs.110).aspx

ページの一番下に移動します(または「スレッドセーフ」というテキストを検索します)。

次のように表示されます。

^スレッドセーフ

このタイプはスレッドセーフです。


7
私は、個人的な経験に基づいて、MSDNの「スレッドセーフ」の定義自体をかなり前に信頼することをやめました。ここに良い読み物があります: リンク
James Legan 2013年

3
その投稿は、上記で提供したリンクとは少し異なります。この区別は、私が提供したリンクがスレッドセーフの宣言に警告を提供しなかったという点で非常に重要です。またMemoryCache.Default、スレッドの問題がまだ発生していない非常に大量(1分あたり数百万のキャッシュヒット)で使用した個人的な経験もあります。
EkoostikMartin 2013年

読み取りと書き込みの操作はアトミックに行われるという意味だと思います。簡単に言うと、スレッドAは現在の値を読み取ろうとしますが、スレッドがメモリにデータを書き込んでいる最中ではなく、常に完了した書き込み操作を読み取ります。メモリに書き込むために、どのスレッドからも介入を行うことはできませんでした。それは私の理解ですが、次のような完全な結論につながらない質問/記事がたくさんあります。stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355

3

.Net 2.0の問題に対処するために、サンプルライブラリをアップロードしました。

このリポジトリを見てください:

RedisLazyCache

Redisキャッシュを使用していますが、接続文字列がない場合はフェイルオーバーまたはメモリキャッシュのみを使用しています。

これはLazyCacheライブラリに基づいており、コールバックの実行に非常にコストがかかる場合に、マルチスレッドがデータをロードおよび保存しようとした場合に、書き込み用のコールバックの単一実行を保証します。


回答のみを共有してください。他の情報はコメントとして共有できます
ElasticCode 2018

1
@WaelAbbas。試しましたが、最初に50の評判が必要なようです。:D。OPの質問に対する直接の回答ではありませんが(理由を説明して「はい/いいえ」で回答できます)、私の回答は「いいえ」の回答の可能な解決策です。
フランシスマラシガン2018

1

@pimbrouwersの回答で@AmitEが述べたように、彼の例はここに示されているように機能していません。

class Program
{
    static async Task Main(string[] args)
    {
        var cache = new MemoryCache(new MemoryCacheOptions());

        var tasks = new List<Task>();
        var counter = 0;

        for (int i = 0; i < 10; i++)
        {
            var loc = i;
            tasks.Add(Task.Run(() =>
            {
                var x = GetOrAdd(cache, "test", TimeSpan.FromMinutes(1), () => Interlocked.Increment(ref counter));
                Console.WriteLine($"Interation {loc} got {x}");
            }));
        }

        await Task.WhenAll(tasks);
        Console.WriteLine("Total value creations: " + counter);
        Console.ReadKey();
    }

    public static T GetOrAdd<T>(IMemoryCache cache, string key, TimeSpan expiration, Func<T> valueFactory)
    {
        return cache.GetOrCreate(key, entry =>
        {
            entry.SetSlidingExpiration(expiration);
            return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
        }).Value;
    }
}

出力:

Interation 6 got 8
Interation 7 got 6
Interation 2 got 3
Interation 3 got 2
Interation 4 got 10
Interation 8 got 9
Interation 5 got 4
Interation 9 got 1
Interation 1 got 5
Interation 0 got 7
Total value creations: 10

GetOrCreate常に作成されたエントリを返すようです。幸いなことに、これは非常に簡単に修正できます。

public static T GetOrSetValueSafe<T>(IMemoryCache cache, string key, TimeSpan expiration,
    Func<T> valueFactory)
{
    if (cache.TryGetValue(key, out Lazy<T> cachedValue))
        return cachedValue.Value;

    cache.GetOrCreate(key, entry =>
    {
        entry.SetSlidingExpiration(expiration);
        return new Lazy<T>(valueFactory, LazyThreadSafetyMode.ExecutionAndPublication);
    });

    return cache.Get<Lazy<T>>(key).Value;
}

それは期待どおりに機能します:

Interation 4 got 1
Interation 9 got 1
Interation 1 got 1
Interation 8 got 1
Interation 0 got 1
Interation 6 got 1
Interation 7 got 1
Interation 2 got 1
Interation 5 got 1
Interation 3 got 1
Total value creations: 1

2
これも機能しません。ちょうどそれを何度も試してみて、あなたは時々値は常に1ではないことがわかります
mlessard

1

キャッシュはスレッドセーフですが、他の人が述べているように、複数のタイプから呼び出す場合、GetOrAddがfuncを複数のタイプで呼び出す可能性があります。

これが私の最小限の修正です

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

そして

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();

それは素晴らしい解決策だと思いますが、異なるキャッシュを変更する複数のメソッドがある場合、ロックを使用すると、それらは必要なくロックされます!この場合、複数の_cacheLockが必要です。キャッシュロックにもキーを含めることができれば、より良いと思います。
ダニエル

これを解決する方法はたくさんあります。1つは、MyCache <T>のインスタンスごとにセマフォが一意になるジェネリックです。次に、AddSingleton(typeof(IMyCache <>)、typeof(MyCache <>));を登録できます。
アンダース

他の一時的なタイプを呼び出す必要がある場合に問題を引き起こす可能性のあるシングルトンをキャッシュ全体にすることはおそらくないでしょう。したがって、シングルトンのセマフォストアICacheLock <T>があるかもしれません
Anders

このバージョンの問題は、同時にキャッシュする2つの異なるものがある場合、2番目のキャッシュをチェックする前に、最初のキャッシュの生成が完了するのを待つ必要があることです。キーが異なる場合は、両方が同時にキャッシュをチェック(および生成)できる方が効率的です。LazyCacheは、Lazy <T>とストライプロックの組み合わせを使用して、アイテムが可能な限り高速にキャッシュされ、キーごとに1回だけ生成されるようにします。参照github.com/alastairtree/lazycacheを
alastairtreeを
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.