GetOrAdd是否会等待,如果它正在检索具有相同键的值?

5

考虑以下代码:

void DoSomething(int key)
{
    concurrentDictionary.GetOrAdd(key, (k)=>
        {
            //Do some expensive over network and database to retrieve value.
        });

考虑有2个线程同时调用DoSomething(2)。同时,它们会发现字典中没有Key==2的项。假设Thread1开始执行昂贵的算法以检索2的值。
问题1: Thread2会等待Thread1完成任务吗?还是尝试自己检索该值,并在添加到字典时丢弃它? (因为Thread1已经添加了这个)
问题2:如果Thread2不等待,那么避免多次运行这个昂贵的算法的最佳解决方案是什么?
2个回答

6

文档中有说明:

如果您在不同的线程上同时调用GetOrAdd,则可能会多次调用addValueFactory,但其键/值对可能不会被添加到字典中。

对于问题2,我建议从ConcurrentDictionary<int,Something>更改为ConcurrentDictionary<int,Lazy<Something>>,其中addValueFactory方法仅构造指定ExecutionAndPublication模式的Lazy<Something>。昂贵的操作将成为Lazy<Something>valueFactory


谢谢。唯一仍然存在的问题是,何时初始化Lazy本身?因为多个线程应该使用相同的Lazy<Something>实例。 - mehrandvd
6
您在addValueFactory中创建了 Lazy<Something>。现在,可能会创建多个 Lazy<Something> 的实例,但是只有一个实际上会最终添加到字典中,而且这个实例也是 GetOrAdd 方法返回的唯一实例。 - Damien_The_Unbeliever

2
考虑有两个线程同时调用 DoSomething(2)。同时,它们会发现字典中没有 Key==2 的项。 我认为加粗部分的情况不可能出现,那将是一种竞争条件。查找 key 是否存在的操作由 GetOrAdd 内部的锁机制保护。无论哪个线程先获取到锁,都有机会添加键值对。其他线程将等待阻塞,然后在第一个线程释放 GetOrAdd 获取的锁之后,直接获取该值。

我错了。@Damien_The_Unbeliever 的答案是正确的。下面是 GetOrAdd 实现的样子。锁仅在 TryGetValue 和 TryAddInternal 中发生。我很想知道背后设计决策的原因。

public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
{
    TValue local;
    if (key == null)
    {
        throw new ArgumentNullException("key");
    }
    if (valueFactory == null)
    {
        throw new ArgumentNullException("valueFactory");
    }
    if (!this.TryGetValue(key, out local))
    {
        this.TryAddInternal(key, valueFactory(key), false, true, out local);
    }
    return local;
}

3
我也认为这是他们做出的一个奇怪决定——如果计算价值如此廉价,你会期望人们使用直接接受值的重载而不是使用值工厂。 - Damien_The_Unbeliever
3
他们很可能这样做是为了避免在锁定状态下调用用户代码时出现死锁。例如,工厂可能会回调字典。BCL 的开发人员非常保守,他们很少在锁定状态下调用用户代码(我实际上从未见过)。 - usr
从另一个角度来看,这个设计决策似乎也是可以的,因为很难单独为值设置锁!例如:如果正在检索键=2的值,而另一个线程想要键=3的值,则不应等待键=2并为键=3添加另一个锁,以便其他线程在需要时等待键=3。 - mehrandvd
2
@mehrandvd,锁被分成了条纹。每个钥匙都分配给一个锁条纹。只有在相同的锁条纹中的钥匙才会发生冲突。这是一种随机而罕见的情况。大多数时候,访问不同的钥匙是独立的。 - usr

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接