【问题标题】:In c# , how to iterate IEnumerable in multithreading environment在 c# 中,如何在多线程环境中迭代 IEnumerable
【发布时间】:2017-06-07 03:32:50
【问题描述】:

我在这种情况下,有一个大字典由一个线程以相当高的频率随机更新,还有另一个线程试图拍摄字典的快照以保存为历史记录。 我目前正在使用这样的东西:

Dictionary<string, object> dict = new Dictionary<string, object>();
var items = dict.Values.ToList();

这在大多数情况下都可以正常工作,只是偶尔会抛出:

System.InvalidOperationException:集合已修改;枚举 操作可能无法执行。

我明白为什么会发生这种情况,但我不知道我该怎么做才能避免集合修改错误。

迭代此类集合的最佳方法是什么?

我也尝试过 ConcurrentDictionary,但没有运气。 为什么? ConcurrentDictionary 线程是否仅在项目级别安全?

【问题讨论】:

  • 你应该使用lock
  • 在我的情况下,这里建议的许多解决方案也可以使用,但接受的解决方案最适合我。抱歉回复晚了。

标签: c# multithreading dictionary concurrency ienumerable


【解决方案1】:

根据the docs,您应该可以使用 ConcurrentDictionary 的GetEnumerator() 方法来获得线程安全的迭代器。

从字典返回的枚举器可以安全地与字典的读取和写入同时使用,但它并不代表字典的即时快照。通过枚举器公开的内容可能包含调用 GetEnumerator 后对字典所做的修改。

由于您正在处理并发线程,因此在一致性方面进行一些权衡并不奇怪,但我希望这种方法比其他答案中给出的蛮力方法阻塞更少。如果您尝试过,这将不起作用:

var items = concurrentDict.Items.ToList();

但它应该适用于

var items = concurrentDict.GetEnumerator();

或者你可以直接引用迭代器:

foreach(var item in concurrentDict)
{
    valueList.Add(item.Value);
}

【讨论】:

  • 我检查了 ToList 源代码,它使用数组复制而不是通过枚举器进行迭代,这可能是 ToList 有时无法工作的原因。
【解决方案2】:

ImmutableDictionary 可能适合您,因为它支持可扩展的多线程快照作为其基本功能集的一部分。

// initialize.
ImmutableDictionary<string, int> dict = ImmutableDictionary.Create<string,int>();

// create a new dictionary with "foo" key added.
ImmutableDictionary<string, int> newdict = dict.Add("foo", 0);

// replace dict, thread-safe, with a new dictionary with "bar" added.
// note this is using dict, not newdict, so there is no "foo" in it.
ImmutableInterlocked.TryAdd(ref dict, "bar", 1);

// take a snapshot, thread-safe.
ImmutableDictionary<string,int> snapshot = dict;

不可变的性质意味着字典永远不会改变——你只能通过创建一个新的字典来添加一个值。由于这个属性,您只需在要快照的点保持引用即可对其进行“快照”。

它在底层进行了优化以提高效率,而不是为每个操作复制整个内容。也就是说,对于其他操作,它不如ConcurrentDictionary 高效,但这都是你想要的权衡。例如,ConcurrentDictionary 可以同时枚举,但无法枚举它的快照。

【讨论】:

    【解决方案3】:

    您可以使用带有lock 关键字的监视器来确保此时仅执行读取或写入。

    public class SnapshotDictionary<TKey, TValue> : IEnumerable<KeyValuePair<TKey, TValue>>
    {
        private readonly Dictionary<TKey, TValue> _dictionary = new Dictionary<TKey, TValue>();
        private readonly object _lock = new object();
    
        public void Add(TKey key, TValue value)
        {
            lock (_lock)
            {
                _dictionary.Add(key, value);
            }
        }
    
        // TODO: Other necessary IDictionary methods
    
        public Dictionary<TKey, TValue> GetSnaphot()
        {
            lock (_lock)
            {
                return new Dictionary<TKey, TValue>(_dictionary);
            }
        }
    
        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return GetSnaphot().GetEnumerator();
        }
    
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }
    }
    

    GetSnapshot 方法返回字典的快照。
    我还覆盖了GetEnumerator,以便它创建一个快照,然后返回快照的枚举器。

    因此,这将起作用,因为将在快照上执行:

    var items = snapshotDictionary.GetSnapshot().Values.ToList();
    
    // or 
    
    foreach (var item in snapshotDictionary)
    {
        // ...
    }
    

    但是,这种方法不允许多线程写入。

    【讨论】:

      猜你喜欢
      • 2023-04-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-09-30
      • 2020-08-22
      相关资源
      最近更新 更多