Á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 添加到字典中?
AddOrUpdate专门设计用于处理您描述的场景;如果它不能优雅地处理它,它将毫无用处。
当线程 B 尝试添加它的计算值时,它会失败,因为键已经存在。然后它会自动再试一次,此时它将执行更新而不是添加。具体来说,它将更新线程 A 产生的值。这是一种乐观并发的形式:算法假设它会成功,因此它会针对该结果进行优化,但它有一个回退计划,以防失败。
但是请注意,此方法的乐观并发性质意味着您的addValueFactory和updateValueFactory可能会同时被调用;它不是严格意义上的两者之一。在您的假设场景中,线程 B 将首先调用 into ,addValueFactory并且由于添加失败,稍后调用 into updateValueFactory。在赛车更新的情况下,updateValueFactory可能会在更新最终成功之前多次调用。
您使用该类的方式ConcurrentDictionary很脆弱。其AddOrUpdate目的是用另一个值替换键的值,而不是修改现有值(如果值是可变对象)。这正是您在updateValueFactory委托中所做的事情:
(_, oldValue) =>
{
oldValue.TryAdd(element, 0);
return oldValue;
}
Run Code Online (Sandbox Code Playgroud)
是oldValuea ConcurrentDictionary<int, byte>,并且通过调用其方法来改变TryAdd。该调用不是同步的,它可能与另一个线程的调用同时发生,甚至可能被每个线程调用多次。从文档中:
但是,在锁外部调用
addValueFactory和updateValueFactory委托是为了避免在锁下执行未知代码时可能出现的问题。因此,AddOrUpdate对于ConcurrentDictionary<TKey,TValue>类上的所有其他操作而言,它不是原子的。
现在,这种特定用法可能意外地是线程安全的,但我个人会避免使用ConcurrentDictionary类似的方法。它看起来像是一个等待发生的错误。
以下是您可以重写代码的方法,以使其不易出错,并且更清楚地了解其意图:
var innerDic = myDic.GetOrAdd(key, _ => new ConcurrentDictionary<int, byte>());
innerDic.TryAdd(element, 0);
Run Code Online (Sandbox Code Playgroud)