Saya berasumsi kode ini memiliki masalah konkurensi:
const string CacheKey = "CacheKey";
static string GetCachedData()
{
string expensiveString =null;
if (MemoryCache.Default.Contains(CacheKey))
{
expensiveString = MemoryCache.Default[CacheKey] as string;
}
else
{
CacheItemPolicy cip = new CacheItemPolicy()
{
AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
};
expensiveString = SomeHeavyAndExpensiveCalculation();
MemoryCache.Default.Set(CacheKey, expensiveString, cip);
}
return expensiveString;
}
Alasan masalah konkurensi adalah bahwa beberapa utas bisa mendapatkan kunci null dan kemudian mencoba memasukkan data ke cache.
Apa cara terpendek dan terbersih untuk membuat kode ini menjadi bukti konkurensi? Saya suka mengikuti pola yang baik di seluruh kode terkait cache saya. Tautan ke artikel online akan sangat membantu.
MEMPERBARUI:
Saya membuat kode ini berdasarkan jawaban @Scott Chamberlain. Adakah yang bisa menemukan masalah kinerja atau konkurensi dengan ini? Jika berhasil, ini akan menghemat banyak baris kode dan kesalahan.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;
namespace CachePoc
{
class Program
{
static object everoneUseThisLockObject4CacheXYZ = new object();
const string CacheXYZ = "CacheXYZ";
static object everoneUseThisLockObject4CacheABC = new object();
const string CacheABC = "CacheABC";
static void Main(string[] args)
{
string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
}
private static string SomeHeavyAndExpensiveXYZCalculation() {return "Expensive";}
private static string SomeHeavyAndExpensiveABCCalculation() {return "Expensive";}
public static class MemoryCacheHelper
{
public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
where T : class
{
//Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;
if (cachedData != null)
{
return cachedData;
}
lock (cacheLock)
{
//Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
cachedData = MemoryCache.Default.Get(cacheKey, null) as T;
if (cachedData != null)
{
return cachedData;
}
//The value still did not exist so we now write it in to the cache.
CacheItemPolicy cip = new CacheItemPolicy()
{
AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
};
cachedData = GetData();
MemoryCache.Default.Set(cacheKey, cachedData, cip);
return cachedData;
}
}
}
}
}
c#
.net
multithreading
memorycache
Allan Xu
sumber
sumber
ReaderWriterLockSlim
?ReaderWriterLockSlim
... Tapi saya juga akan menggunakan teknik ini untuk menghindaritry-finally
pernyataan.Dictionary<string, object>
kunci di mana kuncinya adalah kunci yang sama yang Anda gunakan di AndaMemoryCache
dan objek dalam kamus hanyalah dasar yangObject
Anda kunci. Namun, karena itu, saya akan menyarankan Anda membaca jawaban Jon Hanna. Tanpa pembuatan profil yang tepat Anda mungkin memperlambat program Anda lebih banyak dengan penguncian daripada membiarkan dua contohSomeHeavyAndExpensiveCalculation()
berjalan dan satu hasil dibuang.Jawaban:
Ini adalah iterasi kedua saya dari kode tersebut. Karena
MemoryCache
thread safe, Anda tidak perlu mengunci pembacaan awal, Anda cukup membaca dan jika cache mengembalikan null, lakukan pemeriksaan kunci untuk melihat apakah Anda perlu membuat string. Ini sangat menyederhanakan kode.EDIT : Kode di bawah ini tidak diperlukan tetapi saya ingin membiarkannya menunjukkan metode aslinya. Ini mungkin berguna bagi pengunjung di masa mendatang yang menggunakan koleksi berbeda yang memiliki thread aman membaca tetapi menulis aman non-thread (hampir semua kelas di bawah
System.Collections
namespace seperti itu).Berikut adalah cara saya melakukannya menggunakan
ReaderWriterLockSlim
untuk melindungi akses. Anda perlu melakukan semacam " Penguncian Tercentang Ganda " untuk melihat apakah ada orang lain yang membuat item yang di-cache sementara kami menunggu untuk mengambil kunci.sumber
MemoryCache
kemungkinan besar setidaknya salah satu dari dua hal itu salah.Ada perpustakaan sumber terbuka [penafian: yang saya tulis]: LazyCache bahwa IMO mencakup kebutuhan Anda dengan dua baris kode:
Ini telah dibangun dalam penguncian secara default sehingga metode yang dapat di-cache hanya akan dijalankan satu kali per cache yang hilang, dan menggunakan lambda sehingga Anda dapat melakukan "dapatkan atau tambah" sekaligus. Secara default, kedaluwarsa geser 20 menit.
Bahkan ada paket NuGet ;)
sumber
Saya telah memecahkan masalah ini dengan menggunakan metode AddOrGetExisting di MemoryCache dan penggunaan inisialisasi Lazy .
Pada dasarnya, kode saya terlihat seperti ini:
Skenario terburuk di sini adalah Anda membuat
Lazy
objek yang sama dua kali. Tapi itu sangat sepele. PenggunaanAddOrGetExisting
jaminan bahwa Anda hanya akan mendapatkan satu instance dariLazy
objek, jadi Anda juga dijamin hanya akan memanggil metode inisialisasi mahal sekali.sumber
SomeHeavyAndExpensiveCalculationThatResultsAString()
ada pengecualian, itu macet di cache. Bahkan pengecualian sementara akan disimpan dalam cache denganLazy<T>
: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspxSebenarnya, itu sangat mungkin baik-baik saja, meskipun dengan kemungkinan peningkatan.
Sekarang, secara umum pola di mana kita memiliki beberapa utas yang mengatur nilai bersama pada penggunaan pertama, untuk tidak mengunci nilai yang diperoleh dan disetel dapat berupa:
Namun, mengingat di sini yang
MemoryCache
dapat menggusur entri maka:MemoryCache
merupakan bencana, maka pendekatan yang salah.MemoryCache
thread-safe dalam hal akses ke objek itu, jadi itu bukan masalah di sini.Kedua kemungkinan ini tentunya harus dipikirkan, meskipun satu-satunya saat memiliki dua contoh dari string yang sama dapat menjadi masalah adalah jika Anda melakukan pengoptimalan khusus yang tidak berlaku di sini *.
Jadi, kami memiliki kemungkinan:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.Dan mengerjakannya bisa jadi sulit (memang, hal yang perlu diprofilkan daripada berasumsi Anda bisa menyelesaikannya). Perlu dipertimbangkan di sini meskipun cara paling jelas untuk mengunci penyisipan akan mencegah semua penambahan ke cache, termasuk yang tidak terkait.
Ini berarti bahwa jika kita memiliki 50 utas yang mencoba menetapkan 50 nilai berbeda, maka kita harus membuat semua 50 utas menunggu satu sama lain, meskipun mereka bahkan tidak akan melakukan perhitungan yang sama.
Karena itu, Anda mungkin lebih baik menggunakan kode yang Anda miliki, daripada dengan kode yang menghindari kondisi balapan, dan jika kondisi balapan menjadi masalah, Anda mungkin perlu menanganinya di tempat lain, atau memerlukan kode lain. strategi caching daripada yang membuang entri lama †.
Satu hal yang ingin saya ubah adalah saya akan mengganti panggilan ke
Set()
dengan satu keAddOrGetExisting()
. Dari penjelasan di atas, harus jelas bahwa itu mungkin tidak perlu, tetapi itu akan memungkinkan item yang baru diperoleh untuk dikumpulkan, mengurangi penggunaan memori secara keseluruhan dan memungkinkan rasio yang lebih tinggi dari koleksi generasi rendah ke generasi tinggi.Jadi ya, Anda bisa menggunakan penguncian ganda untuk mencegah konkurensi, tetapi konkurensi sebenarnya bukan masalah, atau Anda menyimpan nilai dengan cara yang salah, atau penguncian ganda di penyimpanan bukanlah cara terbaik untuk menyelesaikannya. .
* Jika Anda mengetahui hanya satu dari setiap rangkaian string yang ada, Anda dapat mengoptimalkan perbandingan kesetaraan, yang kira-kira satu-satunya saat memiliki dua salinan string bisa salah daripada hanya sub-optimal, tetapi Anda ingin melakukannya jenis cache yang sangat berbeda agar masuk akal. Misalnya semacam itu
XmlReader
dilakukan secara internal.† Sangat mungkin salah satu yang menyimpan tanpa batas waktu, atau yang menggunakan referensi lemah sehingga hanya akan mengeluarkan entri jika tidak ada penggunaan.
sumber
Untuk menghindari kunci global, Anda dapat menggunakan SingletonCache untuk mengimplementasikan satu kunci per kunci, tanpa ledakan penggunaan memori (objek kunci dihapus ketika tidak lagi direferensikan, dan perolehan / rilis adalah thread yang aman, menjamin bahwa hanya 1 instance yang pernah digunakan melalui bandingkan dan bertukar).
Menggunakannya terlihat seperti ini:
Kode ada di sini di GitHub: https://github.com/bitfaster/BitFaster.Caching
Ada juga implementasi LRU yang bobotnya lebih ringan daripada MemoryCache, dan memiliki beberapa keunggulan - lebih cepat membaca dan menulis secara bersamaan, ukuran terbatas, tidak ada thread latar belakang, penghitung kinerja internal, dll. (Pelepasan tanggung jawab, saya menulisnya).
sumber
Contoh konsol dari MemoryCache , "Cara menyimpan / mendapatkan objek kelas sederhana"
Output setelah meluncurkan dan menekan Any keykecuali Esc:
Menyimpan ke cache!
Mendapatkan dari cache!
Some1
Some2
sumber
sumber
Ini agak terlambat, namun ... Implementasi penuh:
Ini
getPageContent
tandatangannya:Dan inilah
MemoryCacheWithPolicy
implementasinya:nlogger
hanyalahnLog
objek untuk melacakMemoryCacheWithPolicy
perilaku. Saya membuat ulang cache memori jika request object (RequestQuery requestQuery
) diubah melalui delegate (Func<TParameter, TResult> createCacheData
) atau membuat ulang saat geser atau waktu absolut mencapai batasnya. Perhatikan bahwa semuanya juga asinkron;)sumber
Sulit untuk memilih mana yang lebih baik; lock atau ReaderWriterLockSlim. Anda membutuhkan statistik dunia nyata untuk membaca dan menulis angka dan rasio, dll.
Tetapi jika Anda yakin menggunakan "kunci" adalah cara yang benar. Lalu berikut adalah solusi berbeda untuk kebutuhan yang berbeda. Saya juga menyertakan solusi Allan Xu dalam kode. Karena keduanya bisa dibutuhkan untuk kebutuhan yang berbeda.
Berikut adalah persyaratannya, yang mendorong saya ke solusi ini:
Kode:
sumber