此线程使用此 ConcurrentDictionary 和 AddOrUpdate 方法是否安全?

Is this thread safe with this ConcurrentDictionary and AddOrUpdate method?

我对 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;
    });

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

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

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

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

线程 A 成功插入 key/value 对。

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

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

AddOrUpdate专为处理您描述的场景而设计;如果它不能优雅地处理它,它就没有用了。

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

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

您使用 ConcurrentDictionary class 的方式很脆弱。 AddOrUpdate 用于用另一个值替换键的值,而不是用于修改现有值,以防值是可变对象。这正是您在 updateValueFactory 委托中所做的:

(_, oldValue) =>
{
    oldValue.TryAdd(element, 0);
    return oldValue;
}

oldValue 是一个 ConcurrentDictionary<int, byte>,并通过调用其 TryAdd 方法进行变异。这个调用不是同步的,它可能与另一个线程的调用同时发生,甚至可能被每个线程调用多次。来自 documentation:

However, the addValueFactory and updateValueFactory delegates are called outside the locks to avoid the problems that can arise from executing unknown code under a lock. Therefore, AddOrUpdate is not atomic with regards to all other operations on the ConcurrentDictionary<TKey,TValue> class.

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

您可以通过以下方式重写您的代码,使其不易出错,并使其意图更加清晰:

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