107

对于初学者,让我把它扔在那里,我知道下面的代码不是线程安全的(更正:可能是)。我正在努力寻找一种实现,并且我实际上可以在测试中失败。我现在正在重构一个大型 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;
    }
}
4

7 回答 7

86

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

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

于 2013-11-22T16:46:58.503 回答
62

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

通常这是不可取的——第二个线程应该等待第一个线程完成并使用它的结果,而不是生成两次结果。

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

于 2016-10-17T15:11:34.917 回答
24

正如其他人所说, 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 ToubAsyncLazy<T>在他的 MSDN 上的文章中的出色实现。它将内置的惰性初始化器Lazy<T>与 promise结合起来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());
于 2017-08-22T19:43:40.890 回答
11

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

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

你会看见:

^ 线程安全

这种类型是线程安全的。

于 2013-11-22T20:04:25.447 回答
3

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

看看这个回购:

RedisLazyCache

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

它基于 LazyCache 库,可在多线程尝试加载和保存数据的事件中保证单次执行回调,特别是如果回调执行起来非常昂贵。

于 2018-04-28T00:00:38.510 回答
2

正如@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
于 2018-08-17T09:58:33.540 回答
2

缓存是线程安全的,但正如其他人所说,如果从多种类型调用,GetOrAdd 可能会调用多种类型的 func。

这是我对此的最小修复

private readonly SemaphoreSlim _cacheLock = new SemaphoreSlim(1);

await _cacheLock.WaitAsync();
var data = await _cache.GetOrCreateAsync(key, entry => ...);
_cacheLock.Release();
于 2019-08-29T12:38:33.023 回答