在 Dictionary 和 ConcurrentDictionary 之间修改集合时的不同行为
Different behaviour when collection modified between Dictionary and ConcurrentDictionary
使用下面列表中的普通词典代码,我得到异常
Collection was modified; enumeration operation may not execute.
Dictionary<int, int> dict2 = new Dictionary<int, int>();
dict2.Add(1, 10);
dict2.Add(2, 20);
dict2.Add(3, 30);
dict2.Add(4, 40);
foreach (var d in dict2)
{
if (dict2.ContainsKey(2))
dict2.Remove(2);
if (dict2.ContainsKey(3))
dict2.Remove(3);
}
但是对于 ConcurrentDictionary,这工作正常。
ConcurrentDictionary<int, int> dict1 = new ConcurrentDictionary<int, int>();
dict1.AddOrUpdate(1, 10, (k,v)=> 10);
dict1.AddOrUpdate(2, 20, (k, v) => 20);
dict1.AddOrUpdate(3, 30, (k,v)=> 30);
dict1.AddOrUpdate(4, 40, (k,v)=> 40);
foreach (var d in dict1)
{
int x;
if (dict1.ContainsKey(2))
dict1.TryRemove(2, out x);
if (dict1.ContainsKey(3))
dict1.TryRemove(3, out x);
}
为什么行为会有所不同?
原因是 Dictionary 和 ConcurrentDictionary 有不同的用途。
ConcurrentDictionary - 应该处理并发问题(从不同线程编辑),而 Dictionary 会给你更好的性能。
不同行为的原因是:GetEnumerator() 方法的不同实现。
现在我将解释 Dictionary 异常的原因以及 ConcurrentDictionary 没有异常的原因。
foreach 语句是语法糖,类似于:
var f = dict.GetEnumerator();
while (f.MoveNext())
{
var x = f.Current;
// your logic
}
词典 return 中的 "GetEnumerator()" 结构的新实例名为:"Enumerator"
这个结构实现了:IEnumerator >KeyValuePair>TKey,TValue>>,IDictionaryEnumerator 和他的 C'tor 看起来像:
internal Enumerator(Dictionary<TKey,TValue> dictionary, int getEnumeratorRetType) {
this.dictionary = dictionary;
version = dictionary.version;
index = 0;
this.getEnumeratorRetType = getEnumeratorRetType;
current = new KeyValuePair<TKey, TValue>();
}
"Enumerator" 中 MoveNext() 的实现首先验证源字典未被修改:
bool moveNext(){
if (version != dictionary.version) {
throw new InvalidOperationException....
}
//the itarate over...
}
ConcurrentDictionary 中的 "GetEnumerator()" 实现方式不同:
IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator(){
Node[] buckets = m_tables.m_buckets;
for (int i = 0; i < buckets.Length; i++)
{
Node current = Volatile.Read<Node>(ref buckets[i]);
while (current != null)
{
yield return new KeyValuePair<TKey, TValue>(current.m_key, current.m_value);
current = current.m_next;
}
}
}
在这个实现中有一个叫做 "lazy evaluation" 的技术,return 语句将 return 值。
当消费者调用 MoveNext() 时,您将 return 到 "current = current.m_next;"
所以,在 GetEnumerator().
中没有 "not change" 验证
如果你想避免 "Dictionary editing" 出现异常,那么:
1.迭代到要删除的元素
2.删除元素
3. 在调用 MoveNext() 之前中断
在你的例子中:
foreach (var d in dict2)
{
if (dict2.ContainsKey(1))
dict2.Remove(1);
if (dict2.ContainsKey(3))
dict2.Remove(3);
break; // will prevent from exception
}
有关 ConcurrentDictionary 的 GetEnumerator() 的更多信息:
https://msdn.microsoft.com/en-us/library/dd287131(v=vs.110).aspx
ConcurrentDictionary
的目的是允许多个线程以最少的锁定使用它。如果一个线程希望从一个典型的并发数据结构中接收一个枚举,该枚举表示在某个时刻保存的数据的精确组合,则有必要使用锁来确保在快照的同时不会发生更新结构已构建。即使在使用 ConcurrentDictionary
时,想要构建这种快照的代码也可以使用这种方法。
然而,在许多情况下,代码将对满足以下所有条件的 任何 枚举感到满意:
枚举将包括枚举前存在的所有数据项,在整个枚举过程中继续存在,不加修改。
枚举将不包括枚举期间任何时候集合不包含的任何数据项。
如果在枚举开始时集合不包含某项,但在枚举期间添加and/or修改了N次该项,则枚举应报告该项不超过N次。
如果在枚举开始时集合中包含一个item,并且在枚举过程中添加and/or修改了N次,则枚举将报告该item不超过N+1次。
满足上述条件的枚举方法的成本可能比需要return一个"snapshot"的方法便宜;由于此类枚举通常很有用,因此 ConcurrentDictionary
将其 GetEnumerator
方法定义为 return 更便宜的方法。这种行为不会阻止代码使用外部锁定,但如果唯一可用的枚举器总是拍摄快照,那么当不需要精确的快照时,代码将无法使用更高性能的枚举。
PS--我碰巧认为 ConcurrentDictionary
包含一些明确请求其内容的可枚举快照的方法会有所帮助,即使拍摄这样的快照会相对较慢并且会阻止部分或所有并发访问。即使大型集合的快照太慢而不能经常使用,拥有集合的真实快照在许多调试场景中也是有用的。
使用下面列表中的普通词典代码,我得到异常
Collection was modified; enumeration operation may not execute.
Dictionary<int, int> dict2 = new Dictionary<int, int>();
dict2.Add(1, 10);
dict2.Add(2, 20);
dict2.Add(3, 30);
dict2.Add(4, 40);
foreach (var d in dict2)
{
if (dict2.ContainsKey(2))
dict2.Remove(2);
if (dict2.ContainsKey(3))
dict2.Remove(3);
}
但是对于 ConcurrentDictionary,这工作正常。
ConcurrentDictionary<int, int> dict1 = new ConcurrentDictionary<int, int>();
dict1.AddOrUpdate(1, 10, (k,v)=> 10);
dict1.AddOrUpdate(2, 20, (k, v) => 20);
dict1.AddOrUpdate(3, 30, (k,v)=> 30);
dict1.AddOrUpdate(4, 40, (k,v)=> 40);
foreach (var d in dict1)
{
int x;
if (dict1.ContainsKey(2))
dict1.TryRemove(2, out x);
if (dict1.ContainsKey(3))
dict1.TryRemove(3, out x);
}
为什么行为会有所不同?
原因是 Dictionary 和 ConcurrentDictionary 有不同的用途。 ConcurrentDictionary - 应该处理并发问题(从不同线程编辑),而 Dictionary 会给你更好的性能。
不同行为的原因是:GetEnumerator() 方法的不同实现。
现在我将解释 Dictionary 异常的原因以及 ConcurrentDictionary 没有异常的原因。
foreach 语句是语法糖,类似于:
var f = dict.GetEnumerator();
while (f.MoveNext())
{
var x = f.Current;
// your logic
}
词典 return 中的 "GetEnumerator()" 结构的新实例名为:"Enumerator"
这个结构实现了:IEnumerator >KeyValuePair>TKey,TValue>>,IDictionaryEnumerator 和他的 C'tor 看起来像:
internal Enumerator(Dictionary<TKey,TValue> dictionary, int getEnumeratorRetType) {
this.dictionary = dictionary;
version = dictionary.version;
index = 0;
this.getEnumeratorRetType = getEnumeratorRetType;
current = new KeyValuePair<TKey, TValue>();
}
"Enumerator" 中 MoveNext() 的实现首先验证源字典未被修改:
bool moveNext(){
if (version != dictionary.version) {
throw new InvalidOperationException....
}
//the itarate over...
}
ConcurrentDictionary 中的 "GetEnumerator()" 实现方式不同:
IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator(){
Node[] buckets = m_tables.m_buckets;
for (int i = 0; i < buckets.Length; i++)
{
Node current = Volatile.Read<Node>(ref buckets[i]);
while (current != null)
{
yield return new KeyValuePair<TKey, TValue>(current.m_key, current.m_value);
current = current.m_next;
}
}
}
在这个实现中有一个叫做 "lazy evaluation" 的技术,return 语句将 return 值。 当消费者调用 MoveNext() 时,您将 return 到 "current = current.m_next;" 所以,在 GetEnumerator().
中没有 "not change" 验证如果你想避免 "Dictionary editing" 出现异常,那么: 1.迭代到要删除的元素 2.删除元素 3. 在调用 MoveNext() 之前中断
在你的例子中:
foreach (var d in dict2)
{
if (dict2.ContainsKey(1))
dict2.Remove(1);
if (dict2.ContainsKey(3))
dict2.Remove(3);
break; // will prevent from exception
}
有关 ConcurrentDictionary 的 GetEnumerator() 的更多信息: https://msdn.microsoft.com/en-us/library/dd287131(v=vs.110).aspx
ConcurrentDictionary
的目的是允许多个线程以最少的锁定使用它。如果一个线程希望从一个典型的并发数据结构中接收一个枚举,该枚举表示在某个时刻保存的数据的精确组合,则有必要使用锁来确保在快照的同时不会发生更新结构已构建。即使在使用 ConcurrentDictionary
时,想要构建这种快照的代码也可以使用这种方法。
然而,在许多情况下,代码将对满足以下所有条件的 任何 枚举感到满意:
枚举将包括枚举前存在的所有数据项,在整个枚举过程中继续存在,不加修改。
枚举将不包括枚举期间任何时候集合不包含的任何数据项。
如果在枚举开始时集合不包含某项,但在枚举期间添加and/or修改了N次该项,则枚举应报告该项不超过N次。
如果在枚举开始时集合中包含一个item,并且在枚举过程中添加and/or修改了N次,则枚举将报告该item不超过N+1次。
满足上述条件的枚举方法的成本可能比需要return一个"snapshot"的方法便宜;由于此类枚举通常很有用,因此 ConcurrentDictionary
将其 GetEnumerator
方法定义为 return 更便宜的方法。这种行为不会阻止代码使用外部锁定,但如果唯一可用的枚举器总是拍摄快照,那么当不需要精确的快照时,代码将无法使用更高性能的枚举。
PS--我碰巧认为 ConcurrentDictionary
包含一些明确请求其内容的可枚举快照的方法会有所帮助,即使拍摄这样的快照会相对较慢并且会阻止部分或所有并发访问。即使大型集合的快照太慢而不能经常使用,拥有集合的真实快照在许多调试场景中也是有用的。