这个线程使用这个 ConcurrentDictionary 和 AddOrUpdate 方法安全吗?

Álv*_*cía 2 c# thread-safety concurrentdictionary

我对 C# 中的并发字典有疑问。

在另一个问题中,有人问我如何使用哈希集作为值的并发字典,但使用哈希集并不是一个好主意,最好使用并发字典作为值。所以我得到的解决方案是这样的:

var myDic = new ConcurrentDictionary<long, ConcurrentDictionary<int, byte>>();
myDic.AddOrUpdate(key, 
    _ => new ConcurrentDictionary<int, byte>(new[] {new KeyValuePair<int, byte>(element, 0)}),
    (_, oldValue) => {
        oldValue.TryAdd(element, 0);
        return oldValue;
    });
Run Code Online (Sandbox Code Playgroud)

假设我有两个线程,其中“元素”在线程 A 中为 1,在线程 B 中为 2。

我怀疑这是否是线程安全的。我可能是错的,但我认为并发字典是这样工作的:

线程 A:尝试为键 1 插入元素 1。键 1 不存在,因此尝试使用并发字典插入键 1 ConcurrentDictionary<int, byte>(new[] {new KeyValuePair<int, byte>(1, 0)

线程B:尝试在key 1的字典中插入item 2。 线程A还在添加新的key/value,线程B认为key 1不存在,所以尝试将value添加ConcurrentDictionary<int, byte>(new[] {new KeyValuePair<int, byte>(2, 0)到key 1中。

线程 A 完成以成功插入键/值对。

线程 B 试图完成,但现在键 1 存在,因为线程 A 插入了键 1。因此线程 B 无法插入键/值。

那么会发生什么?线程 B 的工作被丢弃,所以我将在并发字典中只有一项用于键 1?或者线程 B 进入updateValueFactory并将项目 2 添加到字典中?

Mik*_*bel 5

AddOrUpdate专门设计用于处理您描述的场景;如果它不能优雅地处理它,它将毫无用处。

当线程 B 尝试添加它的计算值时,它会失败,因为键已经存在。然后它会自动再试一次,此时它将执行更新而不是添加。具体来说,它将更新线程 A 产生的值。这是一种乐观并发的形式:算法假设它会成功,因此它会针对该结果进行优化,但它有一个回退计划,以防失败。

但是请注意,此方法的乐观并发性质意味着您的addValueFactoryupdateValueFactory可能会同时被调用;它不是严格意义上的两者之一。在您的假设场景中,线程 B 将首先调用 into ,addValueFactory并且由于添加失败,稍后调用 into updateValueFactory。在赛车更新的情况下,updateValueFactory可能会在更新最终成功之前多次调用。

  • 关于细节(特别是“自动重试”以及可以调用两个工厂的事实),ConcurrentDictionary.AddOrUpdate() 的源代码非常易读且有趣,可以查看:http://referencesource.microsoft.com/ #mscorlib/system/Collections/Concurrent/ConcurrentDictionary.cs,e5f450835d8600ed。工厂方法在锁外被调用,以最大限度地减少锁内花费的时间,并且这一切都在一个 while 循环中,以便可以进行多次尝试,直到在并发的情况下成功。 (3认同)

The*_*ias 5

您使用该类的方式ConcurrentDictionary很脆弱。其AddOrUpdate目的是用另一个值替换键的值,而不是修改现有值(如果值是可变对象)。这正是您在updateValueFactory委托中所做的事情:

(_, oldValue) =>
{
    oldValue.TryAdd(element, 0);
    return oldValue;
}
Run Code Online (Sandbox Code Playgroud)

oldValuea ConcurrentDictionary<int, byte>,并且通过调用其方法来改变TryAdd。该调用不是同步的,它可能与另一个线程的调用同时发生,甚至可能被每个线程调用多次。从文档中:

但是,在锁外部调用addValueFactoryupdateValueFactory委托是为了避免在锁下执行未知代码时可能出现的问题。因此,AddOrUpdate对于ConcurrentDictionary<TKey,TValue>类上的所有其他操作而言,它不是原子的。

现在,这种特定用法可能意外地是线程安全的,但我个人会避免使用ConcurrentDictionary类似的方法。它看起来像是一个等待发生的错误。

以下是您可以重写代码的方法,以使其不易出错,并且更清楚地了解其意图:

var innerDic = myDic.GetOrAdd(key, _ => new ConcurrentDictionary<int, byte>());
innerDic.TryAdd(element, 0);
Run Code Online (Sandbox Code Playgroud)