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;
}
}
sumber
Jawaban:
MS default yang disediakan
MemoryCache
sepenuhnya aman untuk thread. Penerapan kustom apa pun yang berasal dariMemoryCache
mungkin tidak aman untuk thread. Jika Anda menggunakan polos diMemoryCache
luar 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
sumber
GetOrCreate
metodenya. ada masalah itu di githubMeskipun MemoryCache memang thread safe seperti jawaban lain yang telah ditentukan, ia memiliki masalah multi-threading yang umum - jika 2 utas mencoba
Get
dari (atau memeriksaContains
) 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 .
sumber
AddOrGetExisting
, daripada menerapkan logika khususContains
. TheAddOrGetExisting
metode MemoryCache adalah atom dan benang referencesource.microsoft.com/System.Runtime.Caching/R/...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.Anda dapat membayangkan bahwa ini akan menjadi sangat buruk jika
TValue
mahal 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 contohLazy<T>
(yang murah).GetOrAdd()
(GetOrCreate()
dalam kasusMemoryCache
) akan mengembalikan hal yang sama, tunggalLazy<T>
ke semua utas, contoh "ekstra"Lazy<T>
akan dibuang begitu saja.Karena
Lazy<T>
tidak melakukan apa pun hingga.Value
dipanggil, hanya satu instance objek yang pernah dibangun.Sekarang untuk beberapa kode! Di bawah ini adalah metode penyuluhan
IMemoryCache
yang mengimplementasikan hal-hal di atas. Ini secara sewenang-wenang diaturSlidingExpiration
berdasarkanint seconds
parameter metode. Tetapi ini sepenuhnya dapat disesuaikan berdasarkan kebutuhan Anda.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 yang
AsyncLazy<T>
ditemukan dalam artikelnya di MSDN. Yang menggabungkan penginisialisasi malas bawaanLazy<T>
dengan janjiTask<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());
sumber
MemoryCache.GetOrCreate
benang tidak aman dengan cara yang samaConcurrentDictionary
adalahLazy
? Jika ya, bagaimana Anda memvalidasi ini?GetOrCreate
menggunakan kunci yang sama dan pabrik itu. hasilnya, pabrik digunakan 10 kali saat menggunakan dengan cache memori (melihat cetakan) + setiap kaliGetOrCreate
mengembalikan nilai yang berbeda! Saya menjalankan pengujian yang sama menggunakanConcurrentDicionary
dan 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 kembaliLihat 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:
sumber
MemoryCache.Default
dalam volume yang sangat tinggi (jutaan cache hits per menit) tanpa masalah threading.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.
sumber
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
GetOrCreate
mengembalikan 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
sumber
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();
sumber