c# - framework - ¿Cómo lidiar con costosas operaciones de construcción usando MemoryCache?
memory cache c# (5)
Aquí hay un diseño que sigue lo que parece tener en mente. El primer bloqueo solo ocurre por un corto tiempo. La última llamada a data.Value también se bloquea (debajo), pero los clientes solo bloquearán si dos de ellos solicitan el mismo elemento al mismo tiempo.
public DataType GetData()
{
lock(_privateLockingField)
{
Lazy<DataType> data = cache["key"] as Lazy<DataType>;
if(data == null)
{
data = new Lazy<DataType>(() => buildDataUsingGoodAmountOfResources();
cache["key"] = data;
}
}
return data.Value;
}
En un proyecto ASP.NET MVC, tenemos varias instancias de datos que requieren buena cantidad de recursos y tiempo para compilar. Queremos almacenarlos en caché.
MemoryCache
proporciona cierto nivel de seguridad de subprocesos pero no lo suficiente como para evitar ejecutar varias instancias de código de construcción en paralelo. Aquí hay un ejemplo:
var data = cache["key"];
if(data == null)
{
data = buildDataUsingGoodAmountOfResources();
cache["key"] = data;
}
Como se puede ver en un sitio web ocupado, cientos de subprocesos podrían ir dentro de la instrucción if simultáneamente hasta que se construyan los datos y hacer que la operación de construcción sea aún más lenta, consumiendo innecesariamente los recursos del servidor.
Hay una implementación atómica AddOrGetExisting
en MemoryCache pero incorrectamente requiere "valor para establecer" en lugar de "código para recuperar el valor para establecer", que creo que hace que el método dado sea casi inútil.
Hemos estado utilizando nuestro propio andamio ad-hoc alrededor de MemoryCache para hacerlo bien, sin embargo requiere lock
explícitas. Es engorroso usar objetos de bloqueo por entrada y generalmente nos alejamos compartiendo objetos de bloqueo, lo que está lejos de ser ideal. Eso me hizo pensar que las razones para evitar tal convención podrían ser intencionales.
Entonces tengo dos preguntas:
¿Es una mejor práctica no
lock
código de construcción? (Eso podría haber demostrado ser más receptivo para uno, me pregunto)¿Cuál es la forma correcta de lograr el bloqueo por entrada para MemoryCache para tal bloqueo? La fuerte necesidad de utilizar
key
cadenakey
como objeto de bloqueo se descarta en ".NET locking 101".
Aquí hay una solución simple como método de extensión MemoryCache.
public static class MemoryCacheExtensions
{
public static T LazyAddOrGetExitingItem<T>(this MemoryCache memoryCache, string key, Func<T> getItemFunc, DateTimeOffset absoluteExpiration)
{
var item = new Lazy<T>(
() => getItemFunc(),
LazyThreadSafetyMode.PublicationOnly // Do not cache lazy exceptions
);
var cachedValue = memoryCache.AddOrGetExisting(key, item, absoluteExpiration) as Lazy<T>;
return (cachedValue != null) ? cachedValue.Value : item.Value;
}
}
Y prueba como descripción de uso.
[TestMethod]
[TestCategory("MemoryCacheExtensionsTests"), TestCategory("UnitTests")]
public void MemoryCacheExtensions_LazyAddOrGetExitingItem_Test()
{
const int expectedValue = 42;
const int cacheRecordLifetimeInSeconds = 42;
var key = "lazyMemoryCacheKey";
var absoluteExpiration = DateTimeOffset.Now.AddSeconds(cacheRecordLifetimeInSeconds);
var lazyMemoryCache = MemoryCache.Default;
#region Cache warm up
var actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
Assert.AreEqual(expectedValue, actualValue);
#endregion
#region Get value from cache
actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
Assert.AreEqual(expectedValue, actualValue);
#endregion
}
Para el requisito de agregar condicional, siempre uso ConcurrentDictionary
, que tiene un método GetOrAdd
sobrecargado que acepta que un delegado se GetOrAdd
si el objeto necesita ser creado.
ConcurrentDictionary<string, object> _cache = new
ConcurrenctDictionary<string, object>();
public void GetOrAdd(string key)
{
return _cache.GetOrAdd(key, (k) => {
//here ''k'' is actually the same as ''key''
return buildDataUsingGoodAmountOfResources();
});
}
En realidad, casi siempre uso diccionarios simultáneos static
. Solía tener diccionarios "normales" protegidos por una instancia de ReaderWriterLockSlim
, pero tan pronto como ReaderWriterLockSlim
a .NET 4 (solo está disponible a partir de ese momento) comencé a convertir cualquiera de los que encontré.
El rendimiento de ConcurrentDictionary
es admirable por decir lo menos :)
Actualice la implementación ingenua con semántica de caducidad según la edad solamente. También debe asegurarse de que los elementos individuales solo se creen una vez, según la sugerencia de @usr. Actualice de nuevo , como ha sugerido @usr, simplemente usar un Lazy<T>
sería mucho más simple: puede reenviar el delegado de creación a eso cuando lo agregue al diccionario concurrente. Cambié el código, ya que de hecho mi diccionario de cerraduras no habría funcionado. Pero realmente debería haberlo pensado (pasada la medianoche aquí en el Reino Unido y estoy vencido. ¿Alguna simpatía? No, por supuesto que no. Como soy desarrollador, tengo suficiente cafeína circulando por mis venas para despertar a los muertos) .
Sin embargo, recomiendo implementar la interfaz IRegisteredObject
con esto y luego registrarlo con el método HostingEnvironment.RegisterObject
eso proporcionaría una forma más limpia de cerrar el hilo del sondeo cuando el grupo de aplicaciones se cierre / recicle.
public class ConcurrentCache : IDisposable
{
private readonly ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>> _cache =
new ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>>();
private readonly Thread ExpireThread = new Thread(ExpireMonitor);
public ConcurrentCache(){
ExpireThread.Start();
}
public void Dispose()
{
//yeah, nasty, but this is a ''naive'' implementation :)
ExpireThread.Abort();
}
public void ExpireMonitor()
{
while(true)
{
Thread.Sleep(1000);
DateTime expireTime = DateTime.Now;
var toExpire = _cache.Where(kvp => kvp.First != null &&
kvp.Item1.Value < expireTime).Select(kvp => kvp.Key).ToArray();
Tuple<string, Lazy<object>> removed;
object removedLock;
foreach(var key in toExpire)
{
_cache.TryRemove(key, out removed);
}
}
}
public object CacheOrAdd(string key, Func<string, object> factory,
TimeSpan? expiry)
{
return _cache.GetOrAdd(key, (k) => {
//get or create a new object instance to use
//as the lock for the user code
//here ''k'' is actually the same as ''key''
return Tuple.Create(
expiry.HasValue ? DateTime.Now + expiry.Value : (DateTime?)null,
new Lazy<object>(() => factory(k)));
}).Item2.Value;
}
}
Solucionamos este problema combinando Lazy<T>
con AddOrGetExisting
para evitar la necesidad de un objeto de bloqueo por completo. Aquí hay un código de muestra (que usa vencimiento infinito):
public T GetFromCache<T>(string key, Func<T> valueFactory)
{
var newValue = new Lazy<T>(valueFactory);
// the line belows returns existing item or adds the new value if it doesn''t exist
var value = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
return (value ?? newValue).Value; // Lazy<T> handles the locking itself
}
Eso no está completo. Hay errores como "caché de excepciones", por lo que debe decidir qué desea hacer en caso de que su valueFactory arroje una excepción. Una de las ventajas, sin embargo, es la capacidad de almacenar en caché valores nulos también.
Tomando la respuesta principal en C # 7, aquí está mi implementación que permite el almacenamiento desde cualquier tipo de fuente T
a cualquier tipo de retorno TResult
.
/// <summary>
/// Creates a GetOrRefreshCache function with encapsulated MemoryCache.
/// </summary>
/// <typeparam name="T">The type of inbound objects to cache.</typeparam>
/// <typeparam name="TResult">How the objects will be serialized to cache and returned.</typeparam>
/// <param name="cacheName">The name of the cache.</param>
/// <param name="valueFactory">The factory for storing values.</param>
/// <param name="keyFactory">An optional factory to choose cache keys.</param>
/// <returns>A function to get or refresh from cache.</returns>
public static Func<T, TResult> GetOrRefreshCacheFactory<T, TResult>(string cacheName, Func<T, TResult> valueFactory, Func<T, string> keyFactory = null) {
var getKey = keyFactory ?? (obj => obj.GetHashCode().ToString());
var cache = new MemoryCache(cacheName);
// Thread-safe lazy cache
TResult getOrRefreshCache(T obj) {
var key = getKey(obj);
var newValue = new Lazy<TResult>(() => valueFactory(obj));
var value = (Lazy<TResult>) cache.AddOrGetExisting(key, newValue, ObjectCache.InfiniteAbsoluteExpiration);
return (value ?? newValue).Value;
}
return getOrRefreshCache;
}
Uso
/// <summary>
/// Get a JSON object from cache or serialize it if it doesn''t exist yet.
/// </summary>
private static readonly Func<object, string> GetJson =
GetOrRefreshCacheFactory<object, string>("json-cache", JsonConvert.SerializeObject);
var json = GetJson(new { foo = "bar", yes = true });