C#:这是ConcurrentDictionary和AddOrUpdate方法的线程安全吗?

时间:2018-02-07 17:39:05

标签: c# concurrentdictionary

我对C#中的并发条款有疑问。

在另一个问题中,我被问到如何将一个带有hashset的并发字典作为值,但是使用hashset不是一个好主意,最好使用并发字典作为值。所以我得到的解决方案是:

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

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

我怀疑这是否是线程安全的。我可能是错的,但我认为并发的限制性工作方式是这样的:

线程A:尝试为密钥1插入元素1.密钥1不存在,因此它尝试使用并发字典ConcurrentDictionary(new [] {new KeyValuePair(1,0))插入密钥1。 / p>

线程B:尝试在键1的字典中插入项目2.线程A仍在添加新的键/值,线程B认为键1不存在,因此尝试添加值ConcurrentDictionary(new [] {new KeyValuePair(2,0)到键1。

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

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

那会发生什么?线程B的工作是丢弃的,所以我只在关键字1的并发字典中有一个项目?或者也许线程B进入ValueFactory并将项目2添加到dicrionary?

感谢。

2 个答案:

答案 0 :(得分:2)

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

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

但请注意,此方法的乐观并发特性意味着您的addValueFactoryupdateValueFactory可能被调用;它不是严格的一个或另一个。在您的假设场景中,线程B将首先调用addValueFactory,因为添加失败,稍后调用updateValueFactory。在赛车更新的情况下,updateValueFactory可能会在更新最终成功之前多次调用。

答案 1 :(得分:1)

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

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

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

<块引用>

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

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

以下是重写代码的方法,以减少出错的可能性,并使其意图更加清晰:

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