web-dev-qa-db-fra.com

Sécurité des threads MemoryCache, le verrouillage est-il nécessaire?

Pour commencer, laissez-moi simplement le jeter, car je sais que le code ci-dessous n’est pas thread-safe (correction: peut-être). Ce qui me pose problème, c’est de trouver une implémentation qui soit une échec possible. Je suis actuellement en train de refactoriser un grand projet WCF qui nécessite (principalement) des données statiques mises en cache et celles-ci provenant d'une base de données SQL. Il doit expirer et "actualiser" au moins une fois par jour, c'est pourquoi j'utilise MemoryCache.

Je sais que le code ci-dessous ne doit pas être thread-safe mais je ne peux pas le faire échouer sous une charge lourde et pour compliquer les choses, une recherche google montre les implémentations dans les deux sens (avec et sans verrous combinés avec des débats si elles sont nécessaires ou non.

Une personne connaissant MemoryCache dans un environnement multithread peut-elle me dire définitivement si j'ai besoin de verrouiller si nécessaire pour qu'un appel à supprimer (qui sera rarement appelé mais qu'il s'agisse d'une exigence) ne soit pas lancé pendant la récupération/repeuplement.

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;
    }
}
55
James Legan

La valeur par défaut MemoryCache fournie par MS est entièrement thread-safe. Toute implémentation personnalisée dérivée de MemoryCache peut ne pas être thread-safe. Si vous utilisez plain MemoryCache immédiatement, le thread est sans danger. Parcourez le code source de ma solution de mise en cache distribuée open source pour voir comment je l’utilise (MemCache.cs):

https://github.com/haneytron/dache/blob/master/Dache.CacheHost/Storage/MemCache.cs

52
Haney

Alors que MemoryCache est effectivement thread-safe, comme le précisent d’autres réponses, il a un problème multi-thread commun: si 2 threads tentent de Get depuis (ou vérifient Contains) le cache en même temps, ils manqueront tous le cache et se termineront jusqu’à générer le résultat et les deux ajouteront ensuite le résultat au cache. 

Souvent, cela n'est pas souhaitable - le second thread doit attendre que le premier soit terminé et utilise son résultat plutôt que de générer deux fois les résultats. 

C'est l'une des raisons pour lesquelles j'ai écrit LazyCache - un wrapper convivial sur MemoryCache qui résout ce genre de problèmes. Il est également disponible sur Nuget .

23
alastairtree

Comme d’autres l’ont dit, MemoryCache est effectivement thread-safe. La sécurité des threads des données qui y sont stockées dépend toutefois entièrement de votre utilisation.

Pour citer Reed Copsey } de son génial post concernant la simultanéité et le type ConcurrentDictionary<TKey, TValue>. Ce qui est bien sûr applicable ici.

Si deux threads appellent simultanément [GetOrAdd], deux instances de TValue peuvent facilement être construites.

Vous pouvez imaginer que cela serait particulièrement grave si TValue est coûteux à construire. 

Pour résoudre ce problème, vous pouvez utiliser très facilement Lazy<T>, dont la construction est par conséquent très peu coûteuse. Cela garantit que si nous entrons dans une situation multithread, nous ne construisons que plusieurs instances de Lazy<T> (ce qui est bon marché). 

GetOrAdd() (GetOrCreate() dans le cas de MemoryCache) renverra le même, Lazy<T> au singulier, dans tous les threads, les instances "supplémentaires" de Lazy<T> seront simplement jetées.

Étant donné que Lazy<T> ne fait rien jusqu'à ce que .Value soit appelé, une seule instance de l'objet est jamais construite.

Maintenant pour du code! Vous trouverez ci-dessous une méthode d'extension pour IMemoryCache qui implémente ce qui précède. Il définit arbitrairement SlidingExpiration en fonction d'un paramètre de méthode int seconds. Mais ceci est entièrement personnalisable en fonction de vos besoins.

Notez que ceci est spécifique aux applications .netcore2.0

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);
}

Appeler:

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());

Pour effectuer cette opération de manière asynchrone, je recommande d'utiliser la mise en œuvre de Stephen Toub _ excellent AsyncLazy<T> trouvée dans son article sur MSDN. Qui combine l'initialisateur paresseux intégré Lazy<T> avec la promesse Task<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())
    { }
}   

Maintenant la version asynchrone 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);
}

Et enfin, appeler:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
11
pimbrouwers

Consultez ce lien: http://msdn.Microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

Allez tout en bas de la page (ou recherchez le texte "Sécurité du fil").

Tu verras:

^ Sécurité des fils

Ce type est thread-safe.

9
EkoostikMartin

Vient de télécharger un exemple de bibliothèque pour résoudre le problème de .Net 2.0. 

Jetez un coup d'oeil sur ce repo: 

RedisLazyCache

J'utilise le cache Redis, mais il effectue également le basculement ou tout simplement Memorycache si Connectionstring est manquant.

Il est basé sur la bibliothèque LazyCache qui garantit une exécution unique du rappel pour l’écriture lorsqu’un multi-threading tente de charger et d’enregistrer des données spécialement si le rappel est très coûteux. 

2
Francis Marasigan

Le cache est threadsafe, mais comme d'autres l'ont indiqué, il est possible que GetOrAdd appelle plusieurs types de fonction si plusieurs appels sont effectués.

Voici ma solution minimale sur ce

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

et

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();
0
Anders

Comme mentionné par @AmitE dans la réponse de @pimbrouwers, son exemple ne fonctionne pas comme illustré ici:

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;
    }
}

Sortie:

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

Il semble que GetOrCreate retourne toujours l’entrée créée. Heureusement, c'est très facile à corriger:

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;
}

Cela fonctionne comme prévu:

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
0
Snicker