4

我正在测试一些同步结构,我注意到一些让我感到困惑的东西。当我枚举一个集合同时写入它时,它抛出了一个异常(这是预期的),但是当我使用 for 循环遍历集合时,它没有。有人可以解释一下吗?我认为 List 不允许读取器和写入器同时操作。我本来希望循环遍历集合会表现出与使用枚举器相同的行为。

更新:这是一个纯粹的学术练习。我知道如果同时写入列表,枚举列表是不好的。我也明白我需要一个同步构造。我的问题再次是关于为什么操作一个按预期抛出异常而另一个没有。

代码如下:

   class Program
   {
    private static List<string> _collection = new List<string>();
    static void Main(string[] args)
    {
        ThreadPool.QueueUserWorkItem(new WaitCallback(AddItems), null);
        System.Threading.Thread.Sleep(5000);
        ThreadPool.QueueUserWorkItem(new WaitCallback(DisplayItems), null);
        Console.ReadLine();
    }

    public static void AddItems(object state_)
    {
        for (int i = 1; i <= 50; i++)
        {
            _collection.Add(i.ToString());
            Console.WriteLine("Adding " + i);
            System.Threading.Thread.Sleep(150);
        }
    }

    public static void DisplayItems(object state_)
    {
        // This will not throw an exception
        //for (int i = 0; i < _collection.Count; i++)
        //{
        //    Console.WriteLine("Reading " + _collection[i]);
        //    System.Threading.Thread.Sleep(150);
        //}

        // This will throw an exception
        List<string>.Enumerator enumerator = _collection.GetEnumerator();
        while (enumerator.MoveNext())
        {
            string value = enumerator.Current;
            System.Threading.Thread.Sleep(150);
            Console.WriteLine("Reading " + value);
        }
    }
}
4

7 回答 7

15

枚举时不能修改集合。即使没有考虑线程问题,该规则仍然存在。来自MSDN

只要集合保持不变,枚举数就保持有效。如果对集合进行了更改,例如添加、修改或删除元素,则枚举器将不可恢复地失效,并且其行为未定义。

基于整数的 for 循环实际上不是枚举数。在大多数情况下是完成同样的事情。但是,IEnumerator 的接口保证您可以遍历整个集合。如果在修改集合后发生对 MoveNext 的调用,则平台通过引发异常在内部强制执行此操作。此异常由枚举器对象引发。

基于整数的 for 循环仅遍历其数字列表。当您按整数索引集合时,您只是在该位置获取项目。如果从列表中插入或删除了某些内容,您可以跳过一个项目或运行相同的项目两次。当您需要在遍历集合时修改集合时,这在某些情况下很有用。for 循环没有枚举器对象来保证 IEnumerator 契约,因此不会引发异常。

于 2009-04-11T01:54:00.880 回答
2

要回答您的实际问题...

枚举时,您将获得一个绑定到列表状态的 IEnumerator,就像您请求它时一样。对枚举器(MoveNext,Current)进行进一步的操作。

使用 for 循环时,如果调用按索引获取特定项目,则您正在创建一个序列。没有诸如枚举器之类的外部上下文知道您处于循环中。对于所有收藏都知道,您只要求一件物品。由于该集合从未分发过枚举器,因此它无法知道您要求第 0 项、第 1 项、第 2 项等的原因是因为您正在遍历列表。

如果您在遍历列表的同时对列表进行处理,那么无论哪种方式,您都会遇到错误。如果添加项目,那么 for 循环可能会默默地跳过一些,而 foreach 循环会抛出。如果删除项目,那么如果你不走运,for 循环可能会抛出一个超出范围的索引,但可能大部分时间都可以工作。

但我想你明白这一切,你的问题只是为什么两种迭代方式的行为不同。答案是当您在一种情况下调用 GetEnumerator 和在另一种情况下调用 get_Item 时,集合的状态是已知的(对集合而言)。

于 2009-04-11T04:46:40.950 回答
1

一旦列表被更改,枚举器就会失效。如果您在枚举列表时更改列表,则需要重新考虑您的策略。

当您开始显示功能时获取一个新的枚举器并在此过程中锁定列表。或者,将您的 List 复制到一个新的 _displayCollection List 中,并枚举这个单独的集合,除非在显示过程开始之前被填充,否则不会写入该集合。希望这可以帮助。

于 2009-04-11T01:41:04.123 回答
1

不同之处在于,当您说“循环遍历集合”时,实际上并不是循环遍历集合,而是遍历 1 到 50 之间的整数,并在这些索引处添加到集合中。这对 1 到 50 之间的数字仍然存在这一事实没有影响。

枚举列表时,您是在枚举项目,而不是索引。因此,当您在枚举时添加项目时,会使枚举无效。它的构建方式是为了防止像您正在做的事情这样的情况,您可能会枚举到列表中的第 6 项,同时在索引 6 处插入一个项目,您可能会枚举旧的或新的项目,或者一些未定义的状态。

如果您想这样做,请查找“线程安全”列表,但要准备好同时处理读+写的不准确性:)

于 2009-04-11T01:52:06.577 回答
1

该列表有一个内部版本计数器,当您更改列表的内容时会更新该计数器。枚举器跟踪版本并在发现列表已更改时抛出异常。

当您只是循环列表时,没有任何东西可以跟踪版本,因此没有任何东西可以捕捉到列表已更改。

如果您在循环时更改列表,您可能会得到不需要的效果,这是枚举器保护您免受的影响。例如,如果您从列表中删除一个项目而不更改循环索引,以便它仍然指向同一个项目,您可能会错过循环中的项目。同样,如果您在不更正索引的情况下插入项目,您可能会多次迭代同一个项目。

于 2009-04-11T01:58:52.863 回答
0

您不能在枚举时更改集合。

问题是您在集合未满时开始枚举,并尝试在枚举时继续添加项目

于 2009-04-11T01:45:32.617 回答
-1

代码有缺陷,因为您睡了 5 秒钟,但并非所有项目都已添加到列表中。这意味着您在第一个线程完成将项目添加到列表之前开始在一个线程上显示项目,从而导致基础集合更改并使枚举器无效。

从 Add 代码中删除 Thread.Sleep 突出显示了这一点:

public static void AddItems(object state_)
{     
   for (int i = 1; i <= 50; i++)      
   {        
       _collection.Add(i.ToString());      
       Console.WriteLine("Adding " + i);  
   }   
} 

您应该使用同步机制来等待第一个线程完成添加项目的工作,而不是休眠。

于 2009-04-11T01:44:10.530 回答