我对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?
感谢。
答案 0 :(得分:2)
AddOrUpdate
专门用于处理您描述的情景;如果它不能优雅地处理它,它将毫无用处。
当线程B尝试添加其计算值时,它将失败,因为该键已存在。然后它将自动重试,此时它将执行更新而不是添加。具体来说,它将更新线程A生成的值。这是乐观并发的一种形式:算法假设它会成功,因此它会优化该结果,但它有一个后备计划,以防万一失败。
但请注意,此方法的乐观并发特性意味着您的addValueFactory
和updateValueFactory
可能都被调用;它不是严格的一个或另一个。在您的假设场景中,线程B将首先调用addValueFactory
,因为添加失败,稍后调用updateValueFactory
。在赛车更新的情况下,updateValueFactory
可能会在更新最终成功之前多次调用。
答案 1 :(得分:1)
您使用 ConcurrentDictionary
类的方式很脆弱。 AddOrUpdate
用于用另一个值替换键的值,而不是用于修改现有值,以防这些值是可变对象。这正是您在 updateValueFactory
委托中所做的:
(_, oldValue) =>
{
oldValue.TryAdd(element, 0);
return oldValue;
}
oldValue
是一个 ConcurrentDictionary<int, byte>
,并通过调用其 TryAdd
方法进行变异。此调用不是同步的,它可能与来自另一个线程的调用并发发生,甚至可能被每个线程多次调用。来自documentation:
然而,addValueFactory
和 updateValueFactory
委托是在锁外调用的,以避免在锁下执行未知代码时可能出现的问题。因此,对于 AddOrUpdate
类上的所有其他操作,ConcurrentDictionary<TKey,TValue>
不是原子的。
现在这种特定用法可能是意外线程安全的,但我个人会避免使用这样的 ConcurrentDictionary
。看起来像是一个等待发生的错误。
以下是重写代码的方法,以减少出错的可能性,并使其意图更加清晰:
var innerDic = myDic.GetOrAdd(key, _ => new ConcurrentDictionary<int, byte>());
innerDic.TryAdd(element, 0);