场景:
- 我有一堆 Child 对象,都与给定的 Parent 相关。
- 我正在开发 ASP.NET MVC 3 Web 应用程序(例如多线程)
- 其中一个页面是“搜索”页面,我需要在其中获取给定的子集子集并在内存中对他们“做一些事情”(计算、排序、枚举)
- 我没有让每个 child 单独调用,而是对数据库进行一次调用以获取给定 parent 的所有 child ,缓存结果并对结果“执行操作”。
- 问题是“做事”涉及 LINQ 操作(枚举、从集合中添加/删除项目),当使用
List<T>
实现时不是线程安全的。
我已经阅读了关于 ConcurrentBag<T>
的内容和 ConcurrentDictionary<T>
但不确定我是否应该使用其中之一,或者自己实现同步/锁定。
我在 .NET 4 上,所以我正在使用 ObjectCache
作为 MemoryCache.Default
的单例实例.我有一个与缓存一起工作的服务层,服务接受 ObjectCache
的实例,这是通过构造函数 DI 完成的。这样所有服务共享相同的 ObjectCache
实例。
主要的线程安全问题是我需要遍历当前的“缓存”集合,如果我正在处理的 child 已经存在,我需要删除它并添加我正在处理的那个,此枚举是导致问题的原因。
有什么建议吗?
最佳答案
是的,要有效地实现缓存,它需要一个快速查找机制等List<T>
是开箱即用的错误数据结构。 Dictionary<TKey, TValue>
是缓存的理想数据结构,因为它提供了一种替换方式:
var value = instance.GetValueExpensive(key);
与:
var value = instance.GetValueCached(key);
通过使用字典中的缓存值并使用字典来完成查找的繁重工作。来电者一无所知。
但是,如果调用者可以从多个线程调用,那么 .NET4 会提供 ConcurrentDictionary<TKey, TValue>
在这种情况下效果很好。但是字典缓存了什么?在您的情况下,字典键似乎是 child ,而字典值是该 child 的数据库结果。
好的,现在我们有一个线程安全且高效的数据库结果缓存,由子键控。我们应该为数据库结果使用什么数据结构?
您没有说这些结果是什么样的,但是由于您使用了 LINQ,我们知道它们至少是 IEnumerable<T>
甚至可能List<T>
.所以我们又回到了同样的问题,对吧?因为List<T>
不是线程安全的,我们不能将它用于字典值。或者我们可以吗?
从调用者的角度来看,缓存必须是只读的。您说使用 LINQ 可以“执行一些操作”,例如添加/删除,但这对于缓存值毫无意义。只有在缓存本身的实现中做一些事情才有意义,例如用新结果替换陈旧的条目。
字典值,因为它是只读的,可以是List<T>
没有不良影响,即使它会被多个线程访问。您可以使用 List<T>.AsReadOnly
以提高您的信心并添加一些编译时安全检查。
但重要的一点是 List<T>
如果它是可变的,则它不是线程安全的。由于根据定义,如果多次调用使用缓存实现的方法必须返回相同的值(直到缓存本身使该值无效),客户端无法修改返回值,因此 List<T>
必须卡住,实际上是不可变的。
如果客户端迫切需要修改缓存的数据库结果并且值为List<T>
, 那么唯一安全的方法是:
- 复印一份
- 修改文案
- 请求缓存更新值
总而言之,对顶级缓存使用线程安全的字典,对缓存值使用普通列表,注意不要在将最后一个插入缓存后修改其内容。
关于c# - 使用集合时如何实现线程安全的缓存机制?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/6132238/