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


91

首先,让我把它扔在那里,我知道下面的代码不是线程安全的(更正:可能)。我正在努力寻找的是一种实现,而我实际上可能在测试中失败。我现在正在重构一个大型WCF项目,该项目需要一些(主要是)静态数据进行缓存,并从SQL数据库中进行填充。它需要至少每天过期一次并“刷新”一次,这就是为什么我要使用MemoryCache。

我知道下面的代码不应该是线程安全的,但是我不能让它在重负载下失败,而且使Google搜索显示两种实现方式的问题复杂化(带锁和不带锁以及是否有必要进行辩论的结合)。

能够在多线程环境中拥有MemoryCache知识的人可以让我确切地知道是否需要在适当的地方锁定,以便在检索/重新填充期间不会引发对remove的调用(很少调用,但这是必需的)。

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

ObjectCache是​​线程安全的,我认为您的类不会失败。msdn.microsoft.com/zh-cn/library/…您可能会同时访问数据库,但这只会使用比所需数量更多的cpu。
the_lotus

1
尽管ObjectCache是​​线程安全的,但其实现可能不是。因此出现了MemoryCache问题。
Haney

Answers:


78

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

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


2
大卫,请确认一下,在上面的非常简单的示例类中,如果在调用Get()的过程中有其他线程,对.Remove()的调用实际上是线程安全的?我想我应该只使用反射器并更深入地进行挖掘,但是那里有很多冲突的信息。
詹姆斯·莱根

12
它是线程安全的,但容易出现争用情况...如果在删除之前发生Get,则数据将在Get上返回。如果删除首先发生,则不会。这很像数据库上的脏读。
Haney

10
值得一提(正如我在下面的另一个回答中所述),dotnet核心实现目前还不是完全线程安全的。具体来说就是GetOrCreate方法。在github上有一个问题
AmitE

使用控制台运行线程时,内存缓存是否引发异常“内存不足”?
Faseeh Haris

49

尽管MemoryCache确实如其他答案所指定的那样是线程安全的,但它确实存在一个常见的多线程问题-如果2个线程试图同时Get从(或检查Contains)高速缓存,则两个都将丢失高速缓存,并且都将最终生成结果,然后两者都将结果添加到缓存中。

通常这是不希望的-第二个线程应该等待第一个线程完成并使用其结果,而不是两次生成结果。

这就是我写LazyCache的原因之一-LazyCache是MemoryCache的友好包装器,可以解决此类问题。它也可以在Nuget使用


“它确实存在一个常见的多线程问题”,这就是为什么您应该使用like之类的原子方法AddOrGetExisting,而不是在周围实现自定义逻辑的原因Contains。所述AddOrGetExisting的MemoryCache的方法是原子和线程 referencesource.microsoft.com/System.Runtime.Caching/R/...
亚历

1
是的,AddOrGetExisting是线程安全的。但是它假定您已经有对要添加到缓存中的对象的引用。通常,您不希望AddOrGetExisting,而希望LazyCache为您提供“ GetExistingOrGenerateThisAndCacheIt”。
9

是的,同意“如果您还没有对象”的要点
Alex

17

正如其他人所说,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);
}

致电:

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

要异步执行所有这些操作,我建议使用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())
    { }
}   

现在是的异步版本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);
}

最后,致电:

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

2
我尝试了一下,但它似乎不起作用(dot net core 2.0)。每个GetOrCreate都会创建一个新的Lazy实例,并使用新的Lazy来更新缓存,因此,多次评估了\创建的Value get(在多线程环境中)。
阿米特

2
从它的外观来看,.netcore 2.0MemoryCache.GetOrCreate并非以相同的方式ConcurrentDictionary是线程安全的
AmitE

可能是一个愚蠢的问题,但您确定是多次创建的价值,而不是价值Lazy?如果是这样,您如何验证这一点?
pimbrouwers 18-3-24

3
我使用了一个工厂函数,该函数在使用时会打印到屏幕上,并生成一个随机数,并启动10个试图GetOrCreate使用同一键和该工厂的线程。结果,与内存缓存一起使用时,工厂被使用了10次(查看打印结果)+每次GetOrCreate返回不同的值!我使用进行了相同的测试ConcurrentDicionary,发现工厂仅使用了一次,并且始终获得相同的值。我在github上发现了一个已关闭的问题,我在那里写了一条评论,指出应该重新打开它
AmitE

警告:此答案不起作用:Lazy <T>不能防止多次执行缓存的函数。请参阅@snicker答案。[net core 2.0]
费尔南多·席尔瓦

11

检查此链接:http : //msdn.microsoft.com/zh-cn/library/system.runtime.caching.memorycache(v=vs.110).aspx

转到页面的最底部(或搜索文本“线程安全”)。

你会看见:

^线程安全

此类型是线程安全的。


7
基于个人经验,我很早以前就不再相信MSDN的“线程安全”定义。这里是一本好书: 链接
James Legan 2013年

3
该帖子与我上面提供的链接略有不同。区别非常重要,因为我提供的链接没有对线程安全声明进行任何警告。我也有大量使用MemoryCache.Default(每分钟数百万次高速缓存命中)的个人经验,而没有线程问题。
EkoostikMartin

我认为它们的意思是读写操作是原子发生的。简而言之,线程A尝试读取当前值时,它总是读取完成的写操作,而不是在任何线程将数据写入内存的过程中。写内存不会有任何线程干预。这是我的理解,但是有很多与此有关的问题/文章并未得出如下完整的结论。stackoverflow.com/questions/3137931/msdn-what-is-thread-safety
erhan355

3

刚刚上传了示例库来解决.Net 2.0的问题。

看一下这个仓库:

RedisLazyCache

我正在使用Redis缓存,但如果缺少Connectionstring,它还会进行故障转移或仅进行Memorycache。

它基于LazyCache库,在多线程尝试加载和保存数据的情况下,如果执行回调非常昂贵,它可以保证在写入时仅执行一次回调回调。


请仅分享答案,其他信息也可以分享为评论
ElasticCode 2018年

1
@WaelAbbas。我尝试过,但看来我首先需要50个声望。:D。尽管这不是对OP问题的直接答案(可以通过“是/否”进行回答,并给出一些解释为什么),但我的答复是针对“否”答案的可能解决方案。
弗朗西斯·马拉西甘

1

正如@AmitE在@pimbrouwers的答案中提到的那样,他的示例无法正常工作,如下所示:

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

输出:

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

似乎GetOrCreate总是返回创建的条目。幸运的是,这很容易解决:

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

可以按预期工作:

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

2
这也不起作用。尝试多次,您会发现有时值并不总是
1。– mlessard

1

缓存是线程安全的,但是就像其他人所说的那样,如果从多个类型调用,则GetOrAdd可能会调用func多个类型。

这是我对此的最小修复

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

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

我认为这是一个不错的解决方案,但是如果我有多种方法来更改不同的缓存,那么如果使用锁,它们将被锁定而无需使用!在这种情况下,我们应该有多个_cacheLocks,我认为如果cachelock也可以有一个Key会更好!
丹尼尔

解决它的方法有很多,一种是泛型的,每个MyCache <T>实例的信号量都是唯一的。然后您可以注册它AddSingleton(typeof(IMyCache <>),typeof(MyCache <>));
安德斯

如果您需要调用其他瞬态类型,我可能不会将整个缓存设置为单例,这可能会导致麻烦。因此,也许有一个信号存储ICacheLock <T>是单例
Anders,

这个版本的问题在于,如果您要同时缓存2个不同的东西,那么您必须等待第一个缓存完成生成,然后才能检查第二个缓存。如果密钥不同,它们可以同时检查缓存(并生成),这对他们来说效率更高。LazyCache使用Lazy <T>和条带化锁定的组合来确保您的项目尽快缓存,并且每个密钥仅生成一次。见github.com/alastairtree/lazycache
9
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.