web-dev-qa-db-ja.com

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

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

私は以下のコードがスレッドセーフであってはならないことを知っていますが、重い負荷の下で失敗することはなく、Google検索が実装を双方向に示す問題を複雑にすることはできません(ロックの有無にかかわらず、ロックが必要かどうかを議論します).

マルチスレッド環境で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;
    }
}
67
James Legan

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

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

63
Haney

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

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

これが私が書いた理由の1つでした LazyCache -この種の問題を解決するMemoryCacheのフレンドリーなラッパーです。 Nuget でも利用可能です。

31
alastairtree

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

Reed Copsey 彼の素晴らしい post から、並行性とConcurrentDictionary<TKey, TValue>タイプについて引用します。もちろん、これはここで適用できます。

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

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

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

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

Lazy<T>.Valueが呼び出されるまで何もしないので、オブジェクトの1つのインスタンスのみが構築されます。

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

これは.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の 記事 にある Stephen Toub's 優れたAsyncLazy<T>実装を使用することをお勧めします。組み込みの遅延イニシャライザーLazy<T>とpromise 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());
13
pim

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

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

表示されます:

^スレッドセーフティ

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

11
EkoostikMartin

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

このレポを見てください:

RedisLazyCache

Redisキャッシュを使用していますが、フェイルオーバー、またはConnectionstringが欠落している場合は単にMemorycacheも使用しています。

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

2

キャッシュはスレッドセーフですが、他の人が述べたように、複数の型から呼び出すとGetOrAddがfuncを複数の型で呼び出す可能性があります。

ここに私の最小限の修正があります

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

そして

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

@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
0
Snicker