我有一个缓存(由 Web 应用程序使用),它在内部使用两个缓存 - 一个短期缓存,仅在请求中使用,以及一个“永久”(跨请求)使用的长期缓存.
我有以下代码,请注意所有底层数据结构都是线程安全的。
public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc)
{
TCache cacheItem;
if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
{
return cacheItem;
}
DateTime cacheDependancyLastModified;
if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)
&& IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified))
{
cacheItem.CacheTime = cacheDependancyLastModified;
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}
cacheItem = cacheItemCreatorFunc(cachdeDependancy);
longTermCache.Add(cachdeDependancy.Id, cacheItem);
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}
显然,上面的代码在并发运行(即多个网络请求)时仍有可能(甚至可能)不一致。 但是我写了一些单元测试,我看到的是从来没有发生“异常”。可能发生的情况是再次添加相同的项目,即使它已经存在等等。 --> 我想您在查看代码时就会明白我的意思。
我仍然认为有一个始终正确且一致的解决方案会很好。
所以我使用一个简单的双重检查锁机制重写了这段代码(也许这会更好,为另一个缓存添加另一个/第二个锁?):
public TCache Get(CacheDependency cachdeDependancy, Func<CacheDependency, TCache> cacheItemCreatorFunc)
{
TCache cacheItem;
if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
{
return cacheItem;
}
lock (_lockObj)
{
if (shortTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem))
{
return cacheItem;
}
DateTime cacheDependancyLastModified;
if (longTermCache.TryGetValue(cachdeDependancy.Id, out cacheItem)
&& IsValid(cachdeDependancy, cacheItem, out cacheDependancyLastModified))
{
cacheItem.CacheTime = cacheDependancyLastModified;
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}
cacheItem = cacheItemCreatorFunc(cachdeDependancy);
longTermCache.Add(cachdeDependancy.Id, cacheItem);
shortTermCache[cachdeDependancy.Id] = cacheItem;
return cacheItem;
}
}
我认为这段代码现在可以在多线程环境中正常工作。
但是我不确定的是: 这不会非常慢,因此也会破坏缓存的目的吗?忍受缓存有时会出现“不一致”行为的问题可能会更好吗? 因为如果同时有1000个web请求,都得等到可以进入lock zone。或者这根本不是问题,因为 CPU 一次只有特定数量的内核(因此是“真正的”并行线程),而且这种性能损失总是很小的?
最佳答案
如果您使用 ConcurrentDictionary
,您已经有办法做您想做的事情 - 您可以简单地使用 GetOrAdd
方法:
shortTermCache[cacheDependency.Id] =
longTermCache.GetOrAdd(cacheDependency.Id, _ => cacheItemCreatorFunc(cachdeDependancy));
快速简单 :)
您甚至可以扩展它以包括短期缓存检查:
return
shortTermCache.GetOrAdd
(
cacheDependency.Id,
_ =>
{
return longTermCache
.GetOrAdd(cacheDependency.Id, __ => cacheItemCreatorFunc(cacheDependency));
}
);
虽然没有必要为每个请求缓存使用 ConcurrentDictionary
- 它实际上不必是线程安全的。
至于您的原始代码,是的,它已损坏。事实上,您在测试期间看不到这一点并不奇怪 - 多线程问题通常很难重现。这就是为什么您首先要编写正确的代码 - 这意味着您必须了解到底发生了什么,以及可能发生什么样的并发问题。在您的情况下,有两个共享引用:longTermCache
和 cacheItem
本身。即使您正在使用的所有对象都是线程安全的,您也无法保证您的代码也是线程安全的 - 在您的情况下,可能存在关于 cacheItem
(这有多线程安全?),或者有人可能同时添加了相同的缓存项。
具体如何中断在很大程度上取决于实际的实现 - 例如,如果具有相同 Id 的项目已经存在,则 Add
可能会抛出异常,也可能不存在。您的代码可能期望所有缓存项都是相同的引用,也可能不是。 cacheItemCreatorFunc
可能会产生可怕的副作用或运行成本很高,也可能不会。
添加了 lock
的更新确实解决了这些问题。但是,例如,它无法处理您到处泄漏 cacheItem
的方式。除非 cacheItem
也是完全线程安全的,否则您可能会遇到一些难以跟踪的错误。我们已经知道它也不是不可变的 - 至少,您正在更改缓存时间。
关于c# - 锁定成本,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33232384/