MemoryCache İş Parçacığı Güvenliği, Kilitlemek Gerekli mi?


92

Yeni başlayanlar için, aşağıdaki kodun iş parçacığı için güvenli olmadığını bildiğimi oraya atmama izin verin (düzeltme: olabilir). Mücadele ettiğim şey, gerçekten test altında başarısız olabileceğim bir uygulama bulmak. Şu anda bazı (çoğunlukla) statik verilerin önbelleğe alınması ve bir SQL veritabanından doldurulması gereken büyük bir WCF projesini yeniden oluşturuyorum. Günde en az bir kez süresinin dolması ve "yenilenmesi" gerekiyor, bu yüzden MemoryCache kullanıyorum.

Aşağıdaki kodun iş parçacığı açısından güvenli olmaması gerektiğini biliyorum, ancak ağır yük altında başarısız olmasını ve sorunları karmaşıklaştırmasını sağlayamıyorum, bir google arama uygulamaları her iki şekilde de gösterir (kilitli ve kilitsiz, gerekli olup olmadıkları tartışmalarla birlikte.

Çok iş parçacıklı bir ortamda MemoryCache bilgisine sahip biri, geri alma / yeniden doldurma sırasında kaldırılacak bir çağrının (nadiren çağrılacak, ancak bir gerekliliktir) atılmaması için uygun olduğunda kilitlemem gerekip gerekmediğini kesin olarak bana söyleyebilir mi?

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 iş parçacığı açısından güvenlidir, sınıfınızın başarısız olabileceğini düşünmüyorum. msdn.microsoft.com/en-us/library/… Veritabanına aynı anda gidebilirsiniz, ancak bu yalnızca gerekenden daha fazla cpu kullanır.
the_lotus

1
ObjectCache iş parçacığı açısından güvenli olsa da, uygulamaları olmayabilir. Böylece MemoryCache sorusu.
Haney

Yanıtlar:


79

Varsayılan MS tarafından sağlanan MemoryCache, tamamen iş parçacığı güvenlidir. Kaynaklanan herhangi bir özel uygulama MemoryCacheiş parçacığı açısından güvenli olmayabilir. MemoryCacheKutudan çıkar çıkmaz kullanıyorsanız , iş parçacığı için güvenlidir. Nasıl kullandığımı görmek için açık kaynak dağıtılmış önbellekleme çözümümün kaynak koduna göz atın (MemCache.cs):

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


2
David, Doğrulamak için, yukarıda sahip olduğum çok basit örnek sınıfta .Remove () çağrısı, Get () çağırma sürecinde farklı bir iş parçacığı varsa aslında iş parçacığı güvenlidir? Sanırım sadece reflektör kullanmalı ve daha derine inmeliyim ama pek çok çelişkili bilgi var.
James Legan

12
İş parçacığı açısından güvenlidir, ancak yarış koşullarına meyillidir ... Al'ınız, Kaldır'ınızdan önce gerçekleşirse, veriler Get'te iade edilir. Önce Kaldır olursa, olmayacaktır. Bu, bir veritabanındaki kirli okumalara çok benzer.
Haney

10
bahsetmeye değer (aşağıda başka bir cevapta yorumladığım gibi), dotnet çekirdek uygulaması şu anda tamamen iş parçacığı güvenli DEĞİLDİR . özellikle GetOrCreateyöntem.
github'da

Konsol kullanarak iş parçacıkları çalıştırırken bellek önbelleği bir "Bellek Yetersiz" istisnası mı atıyor?
Faseeh Haris

50

MemoryCache gerçekten diğer yanıtların belirttiği gibi iş parçacığı açısından güvenli olsa da, ortak bir çoklu iş parçacığı sorununa sahiptir - eğer 2 iş parçacığı aynı anda önbelleği denerse Get(veya kontrol ederse Contains), o zaman her ikisi de önbelleği kaçırır ve her ikisi de üretilir. sonuç ve her ikisi de sonucu önbelleğe ekler.

Çoğu zaman bu istenmeyen bir durumdur - ikinci iş parçacığı, iki kez sonuç oluşturmak yerine ilkinin sonucunu tamamlamasını ve kullanmasını beklemelidir.

Bu, MemoryCache üzerinde bu tür sorunları çözen kullanışlı bir paketleyici olan LazyCache yazmamın nedenlerinden biriydi . Ayrıca Nuget'te de mevcuttur .


"ortak bir çoklu iş parçacığı sorunu var" Bu nedenle AddOrGetExisting, özel mantık uygulamak yerine atomik yöntemler kullanmalısınız Contains. AddOrGetExistingMemoryCache yöntemi atomik ve ÅŸan referencesource.microsoft.com/System.Runtime.Caching/R/...
Alex

1
Evet AddOrGetExisting iş parçacığı açısından güvenlidir. Ancak, önbelleğe eklenecek nesneye zaten bir referansınız olduğunu varsayar. Normalde AddOrGetExisting istemezsiniz "GetExistingOrGenerateThisAndCacheIt" istemezsiniz ki bu LazyCache'nin size verdiği şeydir.
alastairtree

evet, "Eğer halihazırda konuya sahip değilseniz" noktasında anlaştılar
Alex

17

Diğerlerinin de belirttiği gibi, MemoryCache gerçekten iş parçacığı için güvenlidir. Bununla birlikte, içinde depolanan verilerin iş parçacığı güvenliği tamamen sizin kullanımınıza bağlıdır.

Reed Copsey'den eşzamanlılık ve türle ilgili harika gönderisinden alıntı yapmak için ConcurrentDictionary<TKey, TValue>. Elbette burada geçerli olan.

İki iş parçacığı bunu aynı anda [GetOrAdd] olarak çağırırsa, iki TValue örneği kolayca oluşturulabilir.

TValueİnşa edilmesi pahalıysa, bunun özellikle kötü olacağını hayal edebilirsiniz .

Bunun üstesinden Lazy<T>gelmek için, çok kolay bir şekilde yararlanabilirsiniz , bu tesadüfen inşa etmek çok ucuzdur. Bunu yapmak, çok iş parçacıklı bir duruma girersek, bunun yalnızca birden çok örneğini oluşturmamızı sağlar Lazy<T>(ucuzdur).

GetOrAdd()( GetOrCreate()olması durumunda MemoryCache) Lazy<T>tüm iş parçacıklarına tekil olarak aynı şeyi döndürecektir , "ekstra" örnekleri Lazy<T>basitçe atılır.

Yana Lazy<T>kadar bir şey yapmaz .Valuedenir, nesnenin yalnızca bir örneği şimdiye inşa edilmiştir.

Şimdi biraz kod için! Aşağıda, IMemoryCacheyukarıdakileri uygulayan bir genişletme yöntemi bulunmaktadır . İsteğe SlidingExpirationbağlı olarak bir int secondsyöntem parametresine göre ayarlanıyor . Ancak bu, ihtiyaçlarınıza göre tamamen özelleştirilebilir.

Bunun .netcore2.0 uygulamalarına özgü olduğunu unutmayın

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);
}

Aramak:

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

Tüm bunları eşzamansız olarak gerçekleştirmek için, Stephen Toub'un MSDN hakkındaki makalesindeAsyncLazy<T> bulunan mükemmel uygulamasını kullanmanızı öneririm . Tembel başlatıcısı yerleşik hangi birleştirir vaadiyle :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())
    { }
}   

Şimdi şunun eşzamansız sürümü 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);
}

Ve son olarak aramak için:

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

2
Denedim ve çalışmıyor gibi görünüyor (dot net core 2.0). her bir GetOrCreate yeni bir Lazy örneği oluşturur ve önbellek yeni Lazy ile güncellenir ve bu nedenle Değer, birden çok kez değerlendirilir \ oluşturulur (birden çok iş parçacığı ortamında).
AmitE

2
Görünüşe bakılırsa itibaren, 2.0 .netcore MemoryCache.GetOrCreateaynı şekilde iş parçacığı güvenli değil ConcurrentDictionaryise
Amite

Muhtemelen aptalca bir soru, ama birden çok kez yaratılanın Değer olduğundan ve değil Lazymi? Eğer öyleyse, bunu nasıl doğruladınız?
pim

3
Kullanıldığında ekrana yazdıran + rastgele bir sayı üreten ve hepsi GetOrCreateaynı anahtarı ve o fabrikayı kullanmaya çalışan 10 iş parçacığı başlatan bir fabrika işlevi kullandım . Sonuç olarak, fabrika önbelleği ile kullanılırken 10 kez kullanıldı (baskıları gördü) + her seferinde GetOrCreatefarklı bir değer döndürdü! Aynı testi kullanarak ConcurrentDicionaryfabrikanın yalnızca bir kez kullanıldığını gördüm ve her zaman aynı değeri aldım.
Github'da bununla

Uyarı: bu yanıt çalışmıyor: Lazy <T>, önbelleğe alınan işlevin birden çok yürütülmesini engellemiyor. @Snicker yanıtına bakın. [net çekirdek 2.0]
Fernando Silva

11

Bu bağlantıya göz atın: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

Sayfanın en altına gidin (veya "İş Parçacığı Güvenliği" metnini arayın).

Göreceksin:

^ İplik Güvenliği

Bu tür iş parçacığı açısından güvenlidir.


7
Kişisel deneyime dayanarak, MSDN'nin "İş Parçacığı Güvenli" tanımına güvenmeyi bir süre önce bıraktım. İşte iyi bir okuma: bağlantı
James Legan

3
Bu gönderi, yukarıda verdiğim bağlantıdan biraz farklı. Sağladığım bağlantının iş parçacığı güvenliği beyanına herhangi bir uyarı sunmaması açısından ayrım çok önemlidir. Ayrıca, MemoryCache.Defaulthenüz iş parçacığı sorunları olmadan çok yüksek hacimde (dakika başına milyonlarca önbellek isabeti) kullanarak kişisel deneyimim var .
EkoostikMartin

Sanırım onların kastettiği, okuma ve yazma işlemlerinin atomik olarak gerçekleşmesidir.Kısaca, A iş parçacığı mevcut değeri okumaya çalışırken, herhangi bir iş parçacığı tarafından belleğe veri yazmanın ortasında değil, her zaman tamamlanan yazma işlemlerini okur. Hafızaya yazmak, herhangi bir ileti dizisi ile hiçbir müdahale yapılamaz. Anladığım kadarıyla, bununla ilgili aşağıdaki gibi tam bir sonuca götürmeyen birçok soru / makale var. stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355

3

.Net 2.0 sorununu çözmek için örnek kitaplık yükledim.

Bu depoya bir göz atın:

RedisLazyCache

Redis önbelleğini kullanıyorum, ancak Bağlantı dizesi eksikse yük devretme veya yalnızca Bellek Önbelleği kullanıyorum.

Geri aramanın yürütülmesi çok pahalıysa, özel olarak veri yüklemeye ve kaydetmeye çalışan bir çoklu iş parçacığı durumunda yazma için tek bir geri aramanın yürütülmesini garanti eden LazyCache kitaplığına dayanır.


Lütfen yalnızca yanıtı paylaşın, diğer bilgiler yorum olarak paylaşılabilir
ElasticCode

1
@Hayalhanemersin Denedim ama önce 50 itibara ihtiyacım var gibi görünüyor. : D. OP sorusuna doğrudan bir cevap olmasa da (nedenini açıklayarak Evet / Hayır ile cevaplanabilir), cevabım Hayır cevabı için olası bir çözümdür.
Francis Marasigan

1

@AmitE tarafından @pimbrouwers'ın yanıtında belirtildiği gibi, örneği burada gösterildiği gibi çalışmıyor:

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;
    }
}

Çıktı:

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

Her GetOrCreatezaman oluşturulan girdiyi döndürüyor gibi görünüyor . Neyse ki, düzeltmesi çok kolay:

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;
}

Beklendiği gibi çalışıyor:

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
Bu da çalışmıyor. Sadece birçok kez deneyin ve bazen değerin her zaman 1 olmadığını göreceksiniz.
mlessard

1

Önbellek iş parçacığı güvenlidir, ancak diğerlerinin de belirttiği gibi, GetOrAdd'ın birden çok türden çağrı varsa func birden çok türü çağırması mümkündür.

İşte bu konuda minimum düzeltmem

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

ve

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

Bunun güzel bir çözüm olduğunu düşünüyorum, ancak farklı önbellekleri değiştiren birden fazla yöntemim varsa, kilidi kullanırsam ihtiyaç duymadan kilitlenecekler! Bu durumda, birden çok _cacheLock'umuz olmalı, bence önbellek kilidinin de bir Anahtarı olsaydı daha iyi olurdu!
Daniel

Bunu çözmenin birçok yolu vardır, biri jeneriktir, semafor her MyCache <T> örneği için benzersiz olacaktır. Ardından AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
Anders

Muhtemelen tüm önbelleği, diğer geçici türleri çağırmanız gerekirse, sorun yaratabilecek bir tekil yapmazdım. Belki de tekil olan bir semafor deposu ICacheLock <T> olabilir
Anders

Bu sürümle ilgili sorun şu ki, aynı anda önbelleğe alacağınız 2 farklı şey varsa, ikincisi için önbelleği kontrol etmeden önce ilkinin üretmeyi bitirmesini beklemeniz gerekir. Anahtarlar farklıysa, her ikisinin de önbelleği aynı anda kontrol edebilmesi (ve üretebilmesi) için daha verimlidir. LazyCache, öğelerinizin olabildiğince hızlı önbelleğe alınmasını sağlamak ve anahtar başına yalnızca bir kez oluşturmak için Lazy <T> ve şeritli kilitlemenin bir kombinasyonunu kullanır. Bkz. Github.com/alastairtree/lazycache
alastairtree
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.