Presumo que este código tenha problemas de simultaneidade:
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;
}
O motivo do problema de simultaneidade é que vários threads podem obter uma chave nula e, em seguida, tentar inserir dados no cache.
Qual seria a maneira mais curta e limpa de tornar esse código à prova de simultaneidade? Gosto de seguir um bom padrão em meu código relacionado ao cache. Um link para um artigo online seria de grande ajuda.
ATUALIZAR:
Eu vim com este código baseado na resposta de @Scott Chamberlain. Alguém pode encontrar algum problema de desempenho ou concorrência com isso? Se funcionar, muitas linhas de código e erros serão salvos.
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
fonte
fonte
ReaderWriterLockSlim
?ReaderWriterLockSlim
... Mas eu também usaria essa técnica para evitartry-finally
declarações.Dictionary<string, object>
onde a chave é a mesma que você usa no seuMemoryCache
e o objeto no dicionário é apenas um básico queObject
você bloqueia. No entanto, dito isso, recomendo que você leia a resposta de Jon Hanna. Sem um perfil adequado, você pode desacelerar seu programa mais com o bloqueio do que com a permissão de duas instâncias deSomeHeavyAndExpensiveCalculation()
execução e ter um resultado descartado.Respostas:
Esta é minha 2ª iteração do código. Por
MemoryCache
ser thread-safe, você não precisa bloquear a leitura inicial, você pode apenas ler e se o cache retornar nulo, faça a verificação de bloqueio para ver se você precisa criar a string. Isso simplifica muito o código.EDITAR : O código abaixo é desnecessário, mas queria deixá-lo para mostrar o método original. Pode ser útil para futuros visitantes que estão usando uma coleção diferente que tem leituras thread-safe, mas gravações não-thread-safe (quase todas as classes sob o
System.Collections
namespace são assim).Aqui está como eu faria isso usando
ReaderWriterLockSlim
para proteger o acesso. Você precisa fazer uma espécie de " Double Checked Locking " para ver se alguém criou o item em cache enquanto estávamos esperando para pegar o bloqueio.fonte
MemoryCache
é provável que pelo menos uma dessas duas coisas esteja errada.Há uma biblioteca de código aberto [isenção de responsabilidade: que escrevi]: LazyCache que a IMO cobre suas necessidades com duas linhas de código:
Ele tem bloqueio embutido por padrão para que o método armazenável em cache seja executado apenas uma vez por falha de cache e usa um lambda para que você possa "obter ou adicionar" de uma só vez. O padrão é 20 minutos de expiração deslizante.
Existe até um pacote NuGet ;)
fonte
Resolvi esse problema usando o método AddOrGetExisting no MemoryCache e a inicialização lenta .
Essencialmente, meu código se parece com isto:
O pior cenário aqui é que você cria o mesmo
Lazy
objeto duas vezes. Mas isso é bastante trivial. O uso deAddOrGetExisting
garante que você só obterá uma instância doLazy
objeto e, portanto, também terá a garantia de chamar o caro método de inicialização apenas uma vez.fonte
SomeHeavyAndExpensiveCalculationThatResultsAString()
gerou uma exceção, ele ficou preso no cache. Mesmo as exceções transitórias serão armazenadas em cache comLazy<T>
: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspxNa verdade, é bem possível que esteja bem, embora com uma possível melhora.
Agora, em geral, o padrão onde temos vários threads definindo um valor compartilhado no primeiro uso, para não bloquear o valor que está sendo obtido e definido, pode ser:
No entanto, considerando aqui que
MemoryCache
pode despejar entradas então:MemoryCache
é a abordagem errada.MemoryCache
é thread-safe em termos de acesso a esse objeto, portanto, isso não é uma preocupação aqui.Ambas as possibilidades devem ser consideradas, é claro, embora o único momento em que duas instâncias da mesma string existam possa ser um problema é se você estiver fazendo otimizações muito específicas que não se aplicam aqui *.
Então, ficamos com as possibilidades:
SomeHeavyAndExpensiveCalculation()
.SomeHeavyAndExpensiveCalculation()
.E descobrir isso pode ser difícil (na verdade, o tipo de coisa em que vale a pena criar um perfil em vez de presumir que você pode resolvê-lo). No entanto, vale a pena considerar aqui que as formas mais óbvias de travar na inserção evitarão todos adições ao cache, incluindo aquelas não relacionadas.
Isso significa que se tivermos 50 threads tentando definir 50 valores diferentes, teremos que fazer todos os 50 threads esperar uns dos outros, mesmo que eles não façam o mesmo cálculo.
Como tal, você provavelmente está melhor com o código que tem, do que com o código que evita a condição de corrida, e se a condição de corrida for um problema, você provavelmente precisará lidar com isso em outro lugar, ou precisará de um diferente estratégia de cache do que aquela que expulsa entradas antigas †.
A única coisa que eu mudaria é substituir a chamada para
Set()
por uma paraAddOrGetExisting()
. Do exposto, deve ficar claro que provavelmente não é necessário, mas permitiria que o item recém-obtido fosse coletado, reduzindo o uso geral da memória e permitindo uma proporção mais alta de coleções de baixa geração para alta.Então sim, você poderia usar o bloqueio duplo para evitar a simultaneidade, mas ou a simultaneidade não é realmente um problema, ou o armazenamento dos valores da maneira errada, ou o bloqueio duplo na loja não seria a melhor maneira de resolver isso .
* Se você souber que apenas um de cada conjunto de strings existe, você pode otimizar as comparações de igualdade, que é a única vez em que duas cópias de uma string podem estar incorretas em vez de apenas abaixo do ideal, mas você gostaria de fazer muito diferentes tipos de cache para que isso faça sentido. Por exemplo, a classificação
XmlReader
faz internamente.† Muito provavelmente um que armazena indefinidamente ou um que faz uso de referências fracas, de modo que só vai expulsar entradas se não houver usos existentes.
fonte
Para evitar o bloqueio global, você pode usar SingletonCache para implementar um bloqueio por chave, sem explodir o uso de memória (os objetos de bloqueio são removidos quando não são mais referenciados, e adquirir / liberar é thread-safe garantindo que apenas 1 instância esteja em uso por meio de comparação e troca).
Usando a aparência:
O código está aqui no GitHub: https://github.com/bitfaster/BitFaster.Caching
Há também uma implementação LRU que é mais leve do que MemoryCache e tem várias vantagens - leituras e gravações simultâneas mais rápidas, tamanho limitado, sem thread em segundo plano, contadores de desempenho internos etc. (isenção de responsabilidade, eu escrevi).
fonte
Exemplo de console de MemoryCache , "Como salvar / obter objetos de classe simples"
Saída após o lançamento e pressionando, Any keyexceto Esc:
Salvando no cache!
Obtendo do cache!
some1
Some2
fonte
fonte
É um pouco tarde, no entanto ... Implementação completa:
Aqui está a
getPageContent
assinatura:E aqui está a
MemoryCacheWithPolicy
implementação:nlogger
é apenas umnLog
objeto para rastrear oMemoryCacheWithPolicy
comportamento. Recrio o cache de memória se o objeto de solicitação (RequestQuery requestQuery
) for alterado por meio do delegado (Func<TParameter, TResult> createCacheData
) ou recrio quando o tempo deslizante ou absoluto atingir seu limite. Observe que tudo é assíncrono também;)fonte
É difícil escolher qual é o melhor; lock ou ReaderWriterLockSlim. Você precisa de estatísticas do mundo real de números e proporções de leitura e gravação, etc.
Mas se você acredita que usar "lock" é a maneira correta. Então aqui está uma solução diferente para necessidades diferentes. Também incluo a solução de Allan Xu no código. Porque ambos podem ser necessários para necessidades diferentes.
Aqui estão os requisitos que me levam a esta solução:
Código:
fonte