Para começar, deixe-me jogar por aí que sei que o código abaixo não é seguro para threads (correção: pode ser). O que estou lutando é encontrar uma implementação que seja e que eu possa realmente falhar no teste. Estou refatorando um grande projeto WCF agora que precisa de alguns (principalmente) dados estáticos armazenados em cache e preenchidos a partir de um banco de dados SQL. Ele precisa expirar e "atualizar" pelo menos uma vez por dia, por isso estou usando o MemoryCache.
Eu sei que o código abaixo não deve ser thread-safe, mas não consigo fazê-lo falhar sob carga pesada e complicar as coisas, uma pesquisa do Google mostra implementações em ambos os sentidos (com e sem bloqueios combinados com debates se eles são ou não necessários.
Alguém com conhecimento de MemoryCache em um ambiente multiencadeado poderia me informar definitivamente se preciso ou não bloquear onde apropriado para que uma chamada para remover (que raramente será chamada, mas é um requisito) não será lançada durante a recuperação / repopulação.
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;
}
}
fonte
Respostas:
O padrão fornecido pelo MS
MemoryCache
é totalmente seguro para threads. Qualquer implementação customizada que deriva deMemoryCache
pode não ser thread-safe. Se você estiver usando o produto prontoMemoryCache
para uso , ele é seguro para threads. Navegue pelo código-fonte da minha solução de cache distribuído de código aberto para ver como eu o uso (MemCache.cs):https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs
fonte
GetOrCreate
método. há um problema no githubEmbora MemoryCache seja de fato thread-safe como outras respostas especificaram, ele tem um problema comum de multi-threading - se 2 threads tentarem
Get
(ou verificarContains
) o cache ao mesmo tempo, ambos perderão o cache e ambos acabarão gerando o resultado e ambos irão adicionar o resultado ao cache.Freqüentemente, isso é indesejável - o segundo encadeamento deve aguardar a conclusão do primeiro e usar seu resultado em vez de gerar resultados duas vezes.
Esse foi um dos motivos pelos quais escrevi LazyCache - um invólucro amigável do MemoryCache que resolve esse tipo de problema. Também está disponível no Nuget .
fonte
AddOrGetExisting
, em vez de implementar lógica personalizadaContains
. OAddOrGetExisting
método de MemoryCache é atomic e threadsafe referenceource.microsoft.com/System.Runtime.Caching/R/…Como outros afirmaram, MemoryCache é realmente thread-safe. A segurança do thread dos dados armazenados nele, no entanto, depende inteiramente do seu uso.
Para citar Reed Copsey de sua postagem incrível sobre concorrência e o
ConcurrentDictionary<TKey, TValue>
tipo. O que é obviamente aplicável aqui.Você pode imaginar que isso seria especialmente ruim se sua
TValue
construção fosse cara.Para contornar isso, você pode alavancar
Lazy<T>
facilmente, o que, coincidentemente, é muito barato de construir. Fazer isso garante que, se entrarmos em uma situação multithread, estaremos apenas construindo várias instânciasLazy<T>
(o que é barato).GetOrAdd()
(GetOrCreate()
no caso deMemoryCache
) retornará o mesmo, singularLazy<T>
para todos os threads, as instâncias "extras" deLazy<T>
são simplesmente descartadas.Como o
Lazy<T>
não faz nada até.Value
ser chamado, apenas uma instância do objeto é construída.Agora, para algum código! Abaixo está um método de extensão para o
IMemoryCache
qual implementa o acima. É arbitrariamente definido comSlidingExpiration
base em umint seconds
parâmetro de método. Mas isso é totalmente personalizável com base nas suas necessidades.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); }
Chamar:
IMemoryCache cache; var result = cache.GetOrAdd("someKey", 60, () => new object());
Para realizar tudo isso de forma assíncrona, recomendo usar a excelente
AsyncLazy<T>
implementação de Stephen Toub encontrada em seu artigo no MSDN. Que combina o inicializador lento embutidoLazy<T>
com a promessaTask<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()) { } }
Agora, a versão assíncrona de
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); }
E finalmente, para ligar para:
IMemoryCache cache; var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
fonte
MemoryCache.GetOrCreate
não é thread-safe da mesma formaConcurrentDictionary
éLazy
? Se sim, como você validou isso?GetOrCreate
usar a mesma chave e aquela fábrica. o resultado, fábrica foi usado 10 vezes ao usar com cache de memória (viu as impressões) + cada vez queGetOrCreate
retornou um valor diferente! Fiz o mesmo teste usandoConcurrentDicionary
e vi a fábrica sendo usada apenas uma vez, e obtive o mesmo valor sempre. Encontrei um problema fechado nele no github, acabei de escrever um comentário lá que deveria ser reabertoConfira este link: http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx
Vá até o final da página (ou pesquise o texto "Thread Safety").
Você verá:
fonte
MemoryCache.Default
em alto volume (milhões de acessos de cache por minuto) sem problemas de threading ainda.Acabei de carregar a biblioteca de amostras para resolver o problema do .Net 2.0.
Dê uma olhada neste repo:
RedisLazyCache
Estou usando o cache do Redis, mas também failover ou apenas Memorycache se o Connectionstring estiver ausente.
É baseado na biblioteca LazyCache que garante a execução única de callback para escrita em um evento de multi threading tentando carregar e salvar dados, especialmente se o callback for muito caro para executar.
fonte
O cache é threadsafe, mas como outros afirmaram, é possível que GetOrAdd chame a função de vários tipos se for chamada de vários tipos.
Aqui está minha solução mínima para isso
private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);
e
await _cacheLock.WaitAsync(); var data = await _cache.GetOrCreateAsync(key, entry => ...); _cacheLock.Release();
fonte
Conforme mencionado por @AmitE na resposta de @pimbrouwers, seu exemplo não está funcionando conforme demonstrado aqui:
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; } }
Resultado:
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
Parece que
GetOrCreate
retorna sempre a entrada criada. Felizmente, isso é muito fácil de corrigir: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; }
Isso funciona conforme o esperado:
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
fonte