ConcurrentDictionary TryAdd与Item setter性能

时间:2016-08-02 16:47:03

标签: c#

我们正在一个高流量的网站上,我们希望使用ConcurrentDictionary进行一些非常简单的缓存计算,以防止对每个请求执行此操作。可能的输入数量足够有限,计算相对较重(拆分和重新连接字符串)。

我正在考虑像

这样的代码
string result;
if (!MyConCurrentDictionary.TryGetValue(someKey, out result))
{
    result = DoCalculation(someKey);
    // alternative 1: use Item property
    MyConcurrentDictionary[someKey] = result;
    //alternative 2: use TryAdd
    MyConcurrentDictionary.TryAdd(someKey, result);
}

我的问题是:从绩效角度来看,哪种选择是最佳选择?

2 个答案:

答案 0 :(得分:8)

你的代码完全坏了;你假设这两行之间什么都不会改变。

您需要使用.GetOrAdd()

通常,在处理可变并发对象时,必须永远不要对该对象进行多次调用,因为它的状态可以随时改变。

答案 1 :(得分:2)

正如@SLaks所示,您的代码存在竞争条件。 ConcurrentDictionary旨在通过提供执行原子等复杂操作的方法来防止此类情况,例如GetOrAddAddOrUpdate

这是一个描述代码如何破解的序列:

  • 线程1执行TryGetValue,返回false
  • 线程2执行相同的操作
  • 两个线程都进行计算
  • 线程2将值添加到字典并返回结果
  • 线程1
    • (使用索引器设置器)添加另一个值,覆盖前一个值并将其返回
    • (使用TryAdd)没有添加它,并且该方法返回的结果可能与字典中的不同
  • 线程2现在的结果可能与字典
  • 中的结果不同

因此,您可以看到如何不使用GetOrAdd使事情变得更复杂,并可能产生潜在的灾难性后果。在您的情况下,如果您确定性地从密钥中计算字符串,那么它可能无关紧要 - 但实际上没有理由不使用此方法。它还简化了代码,并且可以(略微)提高性能,因为它只计算一次哈希码。

另一个结论是在索引器和TryAdd之间进行选择更多的是正确性而不是性能。

也就是说,索引器[]TryAdd方法的性能是相同的,因为它们共享相同的实现(TryAddInternal),这通过获取属于key的存储桶,然后检查字典中是否存在密钥,以及是否更新字典。

最后,这里的an example展示了如何正确构建GetOrAdd等方法。