Keamanan MemoryCache Thread, Apakah Diperlukan Penguncian?

92

Sebagai permulaan izinkan saya membuangnya di sana bahwa saya tahu kode di bawah ini tidak aman untuk utas (koreksi: mungkin). Apa yang saya perjuangkan adalah menemukan implementasi yang benar-benar bisa gagal dalam pengujian. Saya refactoring proyek WCF besar sekarang yang membutuhkan beberapa (kebanyakan) data statis cache dan diisi dari database SQL. Itu harus kedaluwarsa dan "menyegarkan" setidaknya sekali sehari itulah sebabnya saya menggunakan MemoryCache.

Saya tahu bahwa kode di bawah ini seharusnya tidak thread safe tetapi saya tidak bisa membuatnya gagal di bawah beban berat dan untuk memperumit masalah, pencarian google menunjukkan implementasi kedua cara (dengan dan tanpa kunci dikombinasikan dengan perdebatan apakah perlu atau tidak.

Bisakah seseorang dengan pengetahuan MemoryCache dalam lingkungan multi-threaded memberi tahu saya secara pasti apakah saya perlu mengunci atau tidak di tempat yang sesuai sehingga panggilan untuk menghapus (yang jarang akan dipanggil tetapi persyaratannya) tidak akan muncul selama pengambilan / populasi kembali.

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;
    }
}
James Legan
sumber
ObjectCache adalah thread safe, saya rasa kelas Anda tidak bisa gagal. msdn.microsoft.com/en-us/library/… Anda mungkin membuka database pada saat yang sama tetapi itu hanya akan menggunakan lebih banyak cpu daripada yang dibutuhkan.
the_lotus
1
Meskipun ObjectCache aman untuk thread, implementasinya mungkin tidak. Demikianlah pertanyaan MemoryCache.
Haney

Jawaban:

78

MS default yang disediakan MemoryCachesepenuhnya aman untuk thread. Penerapan kustom apa pun yang berasal dari MemoryCachemungkin tidak aman untuk thread. Jika Anda menggunakan polos di MemoryCacheluar kotak, benang aman. Jelajahi kode sumber solusi caching terdistribusi sumber terbuka saya untuk melihat bagaimana saya menggunakannya (MemCache.cs):

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

Haney
sumber
2
David, Sekadar konfirmasi, dalam contoh kelas yang sangat sederhana yang saya miliki di atas, panggilan ke .Remove () sebenarnya aman untuk thread jika thread berbeda sedang dalam proses memanggil Get ()? Saya kira saya sebaiknya menggunakan reflektor dan menggali lebih dalam tetapi ada banyak info yang bertentangan di luar sana.
James Legan
12
Ini thread aman, tetapi rentan terhadap kondisi balapan ... Jika Get Anda terjadi sebelum Anda Hapus, data akan dikembalikan pada Get. Jika Hapus terjadi lebih dulu, itu tidak akan terjadi. Ini sangat mirip dengan bacaan kotor di database.
Haney
10
layak untuk disebutkan (seperti yang saya komentari di jawaban lain di bawah), implementasi inti dotnet saat ini TIDAK sepenuhnya aman untuk utas. khususnya GetOrCreatemetodenya. ada masalah itu di github
AmitE
Apakah cache memori memunculkan pengecualian "Out of Memory" saat menjalankan utas menggunakan konsol?
Faseeh Haris
50

Meskipun MemoryCache memang thread safe seperti jawaban lain yang telah ditentukan, ia memiliki masalah multi-threading yang umum - jika 2 utas mencoba Getdari (atau memeriksa Contains) cache pada saat yang sama, keduanya akan kehilangan cache dan keduanya akan menghasilkan hasilnya dan keduanya akan menambahkan hasilnya ke cache.

Seringkali hal ini tidak diinginkan - utas kedua harus menunggu utas pertama selesai dan menggunakan hasilnya daripada memberikan hasil dua kali.

Ini adalah salah satu alasan saya menulis LazyCache - pembungkus ramah di MemoryCache yang memecahkan masalah semacam ini. Ini juga tersedia di Nuget .

alastairtree.dll
sumber
"Itu memang memiliki masalah multi threading yang umum" Itulah mengapa Anda harus menggunakan metode atom seperti AddOrGetExisting, daripada menerapkan logika khusus Contains. The AddOrGetExistingmetode MemoryCache adalah atom dan benang referencesource.microsoft.com/System.Runtime.Caching/R/...
Alex
1
Ya AddOrGetExisting aman untuk thread. Tapi ini mengasumsikan Anda sudah memiliki referensi ke objek yang akan ditambahkan ke cache. Biasanya Anda tidak ingin AddOrGetExisting Anda menginginkan "GetExistingOrGenerateThisAndCacheIt" yang diberikan LazyCache kepada Anda.
alastairtree
ya, menyetujui poin "jika Anda belum memiliki obejct"
Alex
17

Seperti yang dinyatakan orang lain, MemoryCache memang thread safe. Keamanan utas dari data yang disimpan di dalamnya, sepenuhnya tergantung pada penggunaan Anda.

Mengutip Reed Copsey dari postingannya yang mengagumkan tentang konkurensi dan ConcurrentDictionary<TKey, TValue>tipe. Yang tentu saja berlaku di sini.

Jika dua utas memanggil ini [GetOrAdd] secara bersamaan, dua contoh TValue dapat dengan mudah dibuat.

Anda dapat membayangkan bahwa ini akan menjadi sangat buruk jika TValuemahal untuk dibangun.

Untuk mengatasi hal ini, Anda dapat memanfaatkan Lazy<T>dengan sangat mudah, yang kebetulan sangat murah untuk dibangun. Melakukan ini memastikan jika kita masuk ke situasi multithread, bahwa kita hanya membangun beberapa contoh Lazy<T>(yang murah).

GetOrAdd()( GetOrCreate()dalam kasus MemoryCache) akan mengembalikan hal yang sama, tunggal Lazy<T>ke semua utas, contoh "ekstra" Lazy<T>akan dibuang begitu saja.

Karena Lazy<T>tidak melakukan apa pun hingga .Valuedipanggil, hanya satu instance objek yang pernah dibangun.

Sekarang untuk beberapa kode! Di bawah ini adalah metode penyuluhan IMemoryCacheyang mengimplementasikan hal-hal di atas. Ini secara sewenang-wenang diatur SlidingExpirationberdasarkan int secondsparameter metode. Tetapi ini sepenuhnya dapat disesuaikan berdasarkan kebutuhan Anda.

Perhatikan ini khusus untuk aplikasi .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);
}

Memanggil:

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

Untuk melakukan ini semua secara asynchronous, saya sarankan menggunakan implementasi yang sangat baik dari Stephen Toub yangAsyncLazy<T> ditemukan dalam artikelnya di MSDN. Yang menggabungkan penginisialisasi malas bawaan Lazy<T>dengan janji 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())
    { }
}   

Sekarang versi asinkron dari 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);
}

Dan terakhir, untuk memanggil:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
pim
sumber
2
Saya mencobanya, dan sepertinya tidak berhasil (dot net core 2.0). setiap GetOrCreate akan membuat instance Lazy baru, dan cache diperbarui dengan Lazy baru, dan karenanya, Value get dievaluasi \ dibuat beberapa kali (dalam beberapa thread env).
AmitE
2
dari kelihatannya, .netcore 2.0 MemoryCache.GetOrCreatebenang tidak aman dengan cara yang sama ConcurrentDictionaryadalah
Amite
Mungkin pertanyaan yang konyol, tetapi apakah Anda yakin bahwa Nilai yang dibuat beberapa kali dan bukan Lazy? Jika ya, bagaimana Anda memvalidasi ini?
pim
3
Saya menggunakan fungsi pabrik yang mencetak ke layar ketika digunakan + menghasilkan nomor acak, dan memulai 10 utas semua mencoba GetOrCreatemenggunakan kunci yang sama dan pabrik itu. hasilnya, pabrik digunakan 10 kali saat menggunakan dengan cache memori (melihat cetakan) + setiap kali GetOrCreatemengembalikan nilai yang berbeda! Saya menjalankan pengujian yang sama menggunakan ConcurrentDicionarydan melihat pabrik digunakan hanya sekali, dan selalu mendapatkan nilai yang sama. Saya menemukan masalah tertutup di github, saya baru saja menulis komentar di sana yang harus dibuka kembali
AmitE
Peringatan: jawaban ini tidak berfungsi: Malas <T> tidak mencegah beberapa eksekusi fungsi yang di-cache. Lihat jawaban @snicker. [inti bersih 2.0]
Fernando Silva
11

Lihat tautan ini: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

Pergi ke bagian paling bawah halaman (atau cari teks "Thread Safety").

Kamu akan lihat:

^ Keamanan Benang

Jenis ini aman untuk benang.

EkoostikMartin
sumber
7
Saya berhenti mempercayai definisi MSDN tentang "Thread Safe" sendiri beberapa waktu yang lalu berdasarkan pengalaman pribadi. Ini bacaan yang bagus: tautan
James Legan
3
Posting itu sedikit berbeda dengan tautan yang saya berikan di atas. Perbedaannya sangat penting karena tautan yang saya berikan tidak menawarkan peringatan apa pun untuk pernyataan keamanan benang. Saya juga memiliki pengalaman pribadi menggunakan MemoryCache.Defaultdalam volume yang sangat tinggi (jutaan cache hits per menit) tanpa masalah threading.
EkoostikMartin
Saya pikir apa yang mereka maksud adalah bahwa operasi baca dan tulis berlangsung secara atomik. Singkatnya, sementara utas A mencoba membaca nilai saat ini, itu selalu membaca operasi tulis yang selesai, bukan di tengah-tengah menulis data ke memori oleh utas apa pun. Saat utas A mencoba untuk menulis ke memori tidak ada intervensi yang dapat dilakukan oleh utas mana pun. Itu pemahaman saya tetapi ada banyak pertanyaan / artikel tentang itu yang tidak mengarah pada kesimpulan lengkap seperti berikut. stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355
3

Baru saja mengunggah perpustakaan sampel untuk mengatasi masalah untuk .Net 2.0.

Lihatlah repo ini:

RedisLazyCache

Saya menggunakan cache Redis tetapi juga failover atau hanya Memorycache jika Connectionstring hilang.

Ini didasarkan pada pustaka LazyCache yang menjamin eksekusi tunggal callback untuk tulis jika terjadi multi-threading yang mencoba memuat dan menyimpan data khususnya jika callback sangat mahal untuk dieksekusi.

Francis Marasigan
sumber
Silakan bagikan jawaban saja, informasi lain dapat dibagikan sebagai komentar
ElasticCode
1
@Waelas. Saya mencoba tetapi tampaknya saya perlu 50 reputasi dulu. : D. Meskipun ini bukan jawaban langsung untuk pertanyaan OP (bisa dijawab dengan Ya / Tidak dengan beberapa penjelasan mengapa), jawaban saya adalah untuk solusi yang mungkin untuk jawaban Tidak.
Francis Marasigan
1

Seperti yang disebutkan oleh @AmitE pada jawaban @pimbrouwers, contohnya tidak berfungsi seperti yang ditunjukkan di sini:

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

Keluaran:

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

Sepertinya GetOrCreatemengembalikan selalu entri yang dibuat. Untungnya, itu sangat mudah diperbaiki:

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

Itu berfungsi seperti yang diharapkan:

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
Kekek
sumber
2
Ini juga tidak berhasil. Coba saja berkali-kali dan Anda akan melihat bahwa terkadang nilainya tidak selalu 1.
mlessard
1

Cache adalah threadsafe, tetapi seperti yang dinyatakan orang lain, mungkin GetOrAdd akan memanggil fungsi beberapa jenis jika panggilan dari beberapa jenis.

Inilah perbaikan minimal saya untuk itu

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

dan

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();
Anders
sumber
Saya pikir ini adalah solusi yang bagus, tetapi jika saya memiliki banyak metode untuk mengubah cache yang berbeda, jika saya menggunakan kunci mereka akan terkunci tanpa perlu! Dalam hal ini kita harus memiliki beberapa _cacheLocks, saya pikir akan lebih baik jika cachelock juga dapat memiliki Key!
Daniel
Ada banyak cara untuk mengatasinya, salah satunya adalah semaphore generik yang akan unik per instance MyCache <T>. Kemudian Anda dapat mendaftarkannya AddSingleton (typeof (IMyCache <>), typeof (MyCache <>));
Anders
Saya mungkin tidak akan membuat seluruh cache menjadi satu-satunya yang dapat menyebabkan masalah jika Anda perlu memanggil jenis transien lainnya. Jadi mungkin memiliki toko semaphore ICacheLock <T> yang tunggal
Anders
Masalah dengan versi ini adalah jika Anda memiliki 2 hal berbeda untuk disimpan dalam cache pada saat yang sama, maka Anda harus menunggu yang pertama selesai dibuat sebelum Anda dapat memeriksa cache untuk yang kedua. Lebih efisien bagi mereka berdua untuk dapat memeriksa cache (dan menghasilkan) pada saat yang sama jika kuncinya berbeda. LazyCache menggunakan kombinasi Lazy <T> dan penguncian bergaris untuk memastikan cache item Anda secepat mungkin, dan hanya menghasilkan sekali per kunci. Lihat github.com/alastairtree/lazycache
alastairtree