确实使用IEnumerable接口减少并发问题

时间:2012-07-25 17:42:27

标签: c# .net collections concurrency ienumerable

根据Albahari brothers[Page No.273],使用IEnumerable是因为:

通过定义返回枚举器的单个方法,IEnumerable提供了灵活性

- >迭代逻辑可以是framed off to another class明白

- > Moreover it means that several consumers can enumerate the collection at once without interfering with each other不理解

我无法理解第二点!

IEnumerable如何使用IEnumerator启用多个消费者一次枚举该集合

4 个答案:

答案 0 :(得分:4)

IEnumerable实现了一个方法GetEnumerator(),该方法返回IEnumerator。因为每次调用该方法时,都会返回一个新的IEnumerator,它具有自己的状态。这样,多个线程可以遍历同一个集合,而不会有一个线程改变另一个线程的当前指针的危险。

如果集合实现IEnumerator,那么它实际上只能由一个线程一次迭代。请考虑以下代码:

public class EnumeratorList : IEnumerator
{
    private object[] _list = new object[10];
    private int _currentIndex = -1;

    public object Current { get { return _list[_currentIndex] } };

    public bool MoveNext()
    {
        return ++_currentIndex < 10;
    }

    public void Reset()
    {
        _currentIndex = -1;
    }
}

鉴于该实现,如果两个线程同时尝试遍历EnumeratorList,它们将获得交错结果,并且不会看到整个列表。

如果我们将它重构为IEnumerable,多个线程可以访问同一个列表而不会出现这些问题。

public class EnumerableList : IEnumerable
{
    private object[] _list = new object[10];

    public IEnumerator GetEnumerator()
    {
        return new ListEnumerator(this);
    }

    private object this[int i]
    {
        return _list[i];
    }

    private class ListEnumerator : IEnumerator
    {
        private EnumeratorList _list;
        private int _currentIndex = -1;

        public ListEnumerator(EnumeratorList list)
        {
            _list = list;
        }

        public object Current { get { return _list[_currentIndex] } };

        public bool MoveNext()
        {
            return ++_currentIndex < 10;
        }

        public void Reset()
        {
            _currentIndex = -1;
        }
    }
}

现在这是一个简单,人为的例子,但我希望这有助于使其更加清晰。

答案 1 :(得分:1)

IEnumerable的{​​{3}}提供了正确使用的一个很好的例子。

要直接回答您的问题,正确实施后,用于浏览集合的IEnumerator对象对每个调用者都是唯一的。这意味着您可以让多个消费者在您的集合上调用foreach,每个消费者都有自己的枚举器,并在集合中有自己的索引。

请注意,这仅提供对集合修改的基本保护。为此,您必须使用正确的lock()块(请参阅MSDN article)。

答案 2 :(得分:1)

考虑代码:

    class Program
{
    static void Main(string[] args)
    {
        var test = new EnumTest();
        test.ConsumeEnumerable2Times();
        Console.ReadKey();
    }
}

public class EnumTest
{
    public IEnumerable<int>  CountTo10()
    {
        for (var i = 0; i <= 10; i++)
            yield return i;
    }

    public void ConsumeEnumerable2Times()
    {
        var enumerable = CountTo10();

        foreach (var n in enumerable)
        {
            foreach (int i in enumerable)
            {
                Console.WriteLine("Outer: {0}, Inner: {1}", n, i);
            }
        }
    }
}

此代码将生成输出:

Outer: 0, Inner: 1
Outer: 0, Inner: 2
...
Outer: 1, Inner: 0
Outer: 1, Inner: 1
...
Outer: 10, Inner: 10

使用IEnumerable,您可以反复枚举相同的集合。 IEnumerable实际上会为每个枚举请求返回一个新的IEnumerator实例。

在上面的示例中,方法EnumTest()被调用一次,但返回的IEnumerable被使用了2次。每次都独立计算到10个。

这就是为什么“几个消费者可以一次列举这个集合而不会互相干扰”。您可以将相同的IEnumerable对象传递给2个方法,它们将独立枚举该集合。使用IEnumerator,你无法实现这一目标。

对不起我的英语。

答案 3 :(得分:1)

实现IEnumerator的类型必须具有方法和属性,以便您可以对其进行迭代,即MoveNext()Reset()Current。如果您有多个线程试图同时迭代此对象会发生什么?他们会相互踩,因为他们都调用相同的MoveNext()函数,这会修改相同的Current属性。

实现IEnumerable的类型必须能够提供IEnumerator的实例。现在当多个线程迭代对象时会发生什么?每个线程都有一个IEnumerator对象的单独实例。返回的IEnumerator与您的集合不是同一类型的对象。它们完全不同。但是,他们确实知道如何获取下一个项目并显示集合的当前项目,并且每个对象都将拥有关于枚举当前状态的内部数据。因此,它们不会相互踩踏并安全地从单独的线程中迭代您的集合。

有时,集合类型会为自己实现IEnumerator(它是它自己的枚举器),然后通过返回自身来实现IEnumerable。在这种情况下,您将无法获得多个线程,因为它们仍然使用相同的对象进行枚举。这是倒退。相反,正确的过程是首先为集合实现单独的(可嵌套的)枚举器类型。然后通过返回该类型的新实例来实现IEnumerable,并通过保留私有实例来实现IEnumerator。