MemoryCache线程安全,锁定是否必要?

Jam*_*gan 73 c# wcf multithreading memorycache

首先让我把它扔出去,我知道下面的代码不是线程安全的(更正:可能).我正在努力的是找到一个实现,并且我实际上可以在测试中失败.我现在正在重构一个大型WCF项目,它需要一些(大部分)静态数据缓存并从SQL数据库中填充.它需要每天至少过期和"刷新"一次,这就是我使用MemoryCache的原因.

我知道下面的代码不应该是线程安全的,但我不能让它在繁重的负载下失败,并且使谷歌搜索显示两种方式的实现变得复杂(有和没有锁结合辩论,无论它们是否必要.

在多线程环境中具有MemoryCache知识的人可以让我明确地知道我是否需要在适当的地方锁定,以便在检索/重新填充期间不会抛出删除调用(很少被调用但是它的要求).

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;
    }
}
Run Code Online (Sandbox Code Playgroud)

Han*_*ney 66

提供的默认MS MemoryCache完全是线程安全的.从派生的任何自定义实现MemoryCache可能不是线程安全的.如果您使用MemoryCache开箱即用,它是线程安全的.浏览我的开源分布式缓存解决方案的源代码,看看我是如何使用它的(MemCache.cs):

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

  • 它是线程安全的,但容易出现竞争条件......如果在删除之前发生Get,则数据将在Get上返回.如果首先发生删除,则不会.这很像数据库上的脏读. (10认同)
  • 值得一提(正如我在下面的另一个答案中评论的那样),**dotnet core**实现目前**不是**完全线程安全.特别是`GetOrCreate`方法.在github上有一个[问题](https://github.com/aspnet/Caching/issues/359) (8认同)
  • 大卫,只是为了确认一下,在我上面的非常简单的示例类中,如果另一个线程正在调用 Get() 的过程中,则对 .Remove() 的调用实际上是线程安全的?我想我应该只使用反射器并深入挖掘,但那里有很多相互矛盾的信息。 (2认同)

ala*_*ree 34

虽然MemoryCache确实是线程安全的,因为其他答案已经指定,但它确实有一个常见的多线程问题 - 如果2个线程同时尝试Get从(或检查Contains)缓存,那么两者都将错过缓存,并且两者都将最终生成结果,然后两者都会将结果添加到缓存中.

通常这是不合需要的 - 第二个线程应该等待第一个线程完成并使用其结果而不是两次生成结果.

这是我写LazyCache的原因之一--LinCache是MemoryCache上的一个友好的包装器,可以解决这些问题.它也可以在Nuget找到.

  • “它确实有一个常见的多线程问题”这就是为什么您应该使用像“AddOrGetExisting”这样的原子方法,而不是围绕“Contains”实现自定义逻辑。MemoryCache 的“AddOrGetExisting”方法**是原子且线程安全的** https://referencesource.microsoft.com/System.Runtime.Caching/R/8a3844239e0386c7.html (3认同)
  • 是的 AddOrGetExisting 是线程安全的。但它假设您已经拥有对将添加到缓存的对象的引用。通常,您不需要 AddOrGetExisting,而是需要“GetExistingOrGenerateThisAndCacheIt”,这正是 LazyCache 为您提供的。 (3认同)
  • 事实上,AddOrGetExisting 不是原子的。请参阅https://github.com/dotnet/runtime/issues/36499 (2认同)

rob*_*pim 14

正如其他人所说,MemoryCache确实是线程安全的.然而,存储在其中的数据的线程安全性完全取决于您的使用.

引用Reed Copsey 关于并发性和ConcurrentDictionary<TKey, TValue>类型的精彩文章.这当然适用于此.

如果两个线程同时调用此[GetOrAdd],则可以轻松构建两个TValue实例.

你可以想象,如果TValue建造起来很昂贵,这将特别糟糕.

为了解决这个问题,你可以Lazy<T>非常轻松地利用它,这恰好是非常便宜的构建.这样做可以确保我们进入多线程情况时,我们只构建多个实例Lazy<T>(这很便宜).

GetOrAdd()(GetOrCreate()在这种情况下MemoryCache)将返回相同的,单数Lazy<T>给所有线程,"额外"实例Lazy<T>被简单地扔掉.

由于在调用Lazy<T>之前不执行任何操作.Value,因此只构造对象的一个​​实例.

现在换一些代码!以下是IMemoryCache实现上述内容的扩展方法.它任意地SlidingExpiration基于int seconds方法参数进行设置.但这完全可以根据您的需求进行定制.

请注意,这是特定于.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);
}
Run Code Online (Sandbox Code Playgroud)

致电:

IMemoryCache cache;
var result = cache.GetOrAdd("someKey", 60, () => new object());
Run Code Online (Sandbox Code Playgroud)

为了全部异步执行此操作,我建议使用Stephen Toub在MSDN上的文章中优秀AsyncLazy<T>实现.它结合了内置的懒惰初始化程序和承诺:Lazy<T>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())
    { }
}   
Run Code Online (Sandbox Code Playgroud)

现在的异步版本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);
}
Run Code Online (Sandbox Code Playgroud)

最后,致电:

IMemoryCache cache;
var result = await cache.GetOrAddAsync("someKey", 60, async () => new object());
Run Code Online (Sandbox Code Playgroud)

  • 我使用了一个工厂函数,该函数在使用时会打印到屏幕上,并生成一个随机数,并启动10个线程,所有线程都试图使用相同的键和该工厂来“ GetOrCreate”。结果,与内存缓存一起使用时,工厂被使用了10次(查看打印结果)+每次`GetOrCreate`返回不同的值!我使用`ConcurrentDicionary`运行了相同的测试,发现工厂仅被使用了一次,并且总是得到相同的值。我在github上发现了一个已关闭的[issue](https://github.com/aspnet/Caching/issues/359),我在那儿写了一条评论,说应该重新打开它 (3认同)
  • 我尝试了一下,但它似乎不起作用(dot net core 2.0)。每个GetOrCreate都将创建一个新的Lazy实例,并使用新的Lazy来更新缓存,因此,多次(在多线程环境中)对Value get进行了评估\创建。 (2认同)
  • 从它的外观来看,.netcore 2.0`MemoryCache.GetOrCreate`与`ConcurrentDictionary`是不一样的线程安全 (2认同)

Eko*_*tin 11

看看这个链接:http://msdn.microsoft.com/en-us/library/system.runtime.caching.memorycache(v=vs.110).aspx

转到页面的最底部(或搜索文本"Thread Safety").

你会看见:

^线程安全

这种类型是线程安全的.

  • 基于个人经验,我很久以前就不再信任MSDN对"线程安全"的定义了.这是一个很好的阅读:[link](http://stackoverflow.com/questions/3137931/msdn-what-is-thread-safety) (6认同)
  • 该帖子与我上面提供的链接略有不同.区别非常重要,因为我提供的链接没有提供线程安全声明的任何警告.我也有使用"MemoryCache.Default"的非常高的体积(每分钟数百万次高速缓存命中)的个人经验,但没有线程问题. (2认同)