在c#中,如何在multithreading环境中迭代IEnumerable
我在这种情况下,有一个大字典,由一个线程以相当高的频率随机更新,还有另一个线程试图将字典的快照保存为历史记录。 我目前正在使用这样的东西:
Dictionary dict = new Dictionary(); var items = dict.Values.ToList();
这在大多数情况下都可以正常工作,除非它偶尔抛出:
System.InvalidOperationException:集合已被修改; 枚举操作可能无法执行。
我理解为什么会这样,但我不知道我该怎么做才能避免收集修改错误。
迭代此类集合的最佳方法是什么?
我也试过ConcurrentDictionary,但没有运气。 为什么? ConcurrentDictionary线程只在项目级别安全吗?
根据文档,您应该能够使用ConcurrentDictionary的GetEnumerator()
方法来获取线程安全的迭代器。
从字典返回的枚举器可以安全地与字典的读写一起使用,但它不代表字典的即时快照。 通过枚举器公开的内容可能包含在调用GetEnumerator后对字典所做的修改。
既然你正在处理并发线程,那么在一致性方面进行一些权衡并不奇怪,但我希望这种方法能够阻止其他答案中给出的暴力方法。 如果您尝试过,这将不起作用:
var items = concurrentDict.Items.ToList();
但它应该适用于
var items = concurrentDict.GetEnumerator();
或者您可以直接引用迭代器:
foreach(var item in concurrentDict) { valueList.Add(item.Value); }
ImmutableDictionary
可能适合您,因为它支持可扩展的multithreading和快照作为其基本function集的一部分。
// initialize. ImmutableDictionary dict = ImmutableDictionary.Create(); // create a new dictionary with "foo" key added. ImmutableDictionary 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 snapshot = dict;
不可变性意味着字典永远不会改变 – 您只能通过创建新字典来添加值。 而且由于这个属性,你可以通过简单地从想要快照的点保持一个参考来获取它的“快照”。
它在引擎盖下进行优化以提高效率,而不是为每个操作复制整个事物。 也就是说,对于其他操作来说,它不如ConcurrentDictionary
那么高效,但它只是你想要的权衡。 例如,可以并发枚举ConcurrentDictionary
但不可能枚举它的快照。
您可以使用带有lock
关键字的监视器来确保此时仅执行读取或仅执行写入。
public class SnapshotDictionary : IEnumerable> { private readonly Dictionary _dictionary = new Dictionary(); 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 GetSnaphot() { lock (_lock) { return new Dictionary(_dictionary); } } public IEnumerator> GetEnumerator() { return GetSnaphot().GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } }
GetSnapshot
方法返回字典的快照。
我还覆盖了GetEnumerator
以便它创建一个快照,然后返回快照的枚举器。
因此,这将起作用,因为将在快照上执行:
var items = snapshotDictionary.GetSnapshot().Values.ToList(); // or foreach (var item in snapshotDictionary) { // ... }
但是,这种方法不允许multithreading写入。