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

时间:2013-11-22 16:30:27

标签: c# multithreading wcf 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;
    }
}

7 个答案:

答案 0 :(得分:60)

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

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

答案 1 :(得分:27)

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

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

这是我编写LazyCache的原因之一 - MemoryCache上的友好包装解决了这些问题。它也可以在Nuget上找到。

答案 2 :(得分:12)

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

引用他真棒Reed Copsey中有关并发性和ConcurrentDictionary<TKey, TValue>类型的post。这当然适用于此。

  

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

你可以想象如果TValue构建起来很昂贵,那将会特别糟糕。

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

GetOrAdd()(在GetOrCreate()的情况下为MemoryCache)将向所有线程返回相同的单数Lazy<T>,&#34; extra&#34; Lazy<T>的实例被丢弃了。

由于Lazy<T>在调用.Value之前没有做任何事情,因此只构造了一个对象实例。

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

  

请注意,这是特定于.netcore2.0 apps

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

要以异步方式执行此操作,建议您在MSDN上的Stephen Toub's中使用article优秀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());

答案 3 :(得分:11)

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

转到页面的最底部(或搜索文本“Thread Safety”)。

你会看到:

  

^线程安全

     

此类型是线程安全的。

答案 4 :(得分:2)

刚上传样本库以解决.Net 2.0的问题。

看看这个回购:

RedisLazyCache

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

它基于LazyCache库,保证在多线程事件中单独执行回调以便在执行回调非常昂贵的情况下尝试加载和保存数据。

答案 5 :(得分:0)

正如@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

答案 6 :(得分:0)

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

这是我对此的最小修正

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

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