在c#中,如何在多线程环境下迭代IEnumerable

In c# , how to iterate IEnumerable in multithreading environment

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

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

这在大多数情况下工作正常,除了偶尔会抛出:

System.InvalidOperationException: Collection was modified; enumeration operation may not execute.

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

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

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

您可以使用带有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 方法 returns 你的字典的快照。
我还覆盖了 GetEnumerator 以便它创建快照,然后创建 returns 快照的枚举器。

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

var items = snapshotDictionary.GetSnapshot().Values.ToList();

// or 

foreach (var item in snapshotDictionary)
{
    // ...
}

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

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

The enumerator returned from the dictionary is safe to use concurrently with reads and writes to the dictionary, however it does not represent a moment-in-time snapshot of the dictionary. The contents exposed through the enumerator may contain modifications made to the dictionary after GetEnumerator was called.

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

var items = concurrentDict.Items.ToList();

但它应该适用于

var items = concurrentDict.GetEnumerator();

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

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

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;

不可变的性质意味着字典永远不会改变——您只能通过创建新字典来添加一个值。由于这个 属性,您只需在要拍摄快照的点周围保留一个参考即可拍摄 "snapshot"。

它在后台进行了优化以提高效率,而不是为每个操作复制整个内容。也就是说,对于其他操作,它的效率不如 ConcurrentDictionary,但这都是您想要的权衡。例如,ConcurrentDictionary可以并发枚举,但不可能枚举它的快照。