我正在测试一些同步结构,我注意到一些让我感到困惑的事情。当我在同时写入一个集合时枚举它时,它抛出了异常(这是预期的),但是当我使用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);
}
}
}
答案 0 :(得分:15)
在枚举时不能修改集合。即使没有考虑线程问题,该规则也存在。来自MSDN:
只要集合保持不变,枚举器仍然有效。如果对集合进行了更改,例如添加,修改或删除元素,则枚举数将无法恢复,并且其行为未定义。
基于整数的for循环实际上不是枚举器。在大多数情况下,完成同样的事情。但是,IEnumerator的接口保证您可以遍历整个集合。如果在修改集合后发生对MoveNext的调用,则平台会通过抛出异常来内部强制执行此操作。枚举器对象抛出此异常。
基于整数的for循环只会遍历其数字列表。当您按整数索引集合时,您只是将项目放在该位置。如果已从列表中插入或删除某些内容,您可以跳过某个项目或两次运行相同的项目。当您需要在遍历集合时修改集合时,这在某些情况下非常有用。 for循环没有枚举器对象来保证IEnumerator契约,因此不会抛出任何异常。
答案 1 :(得分:2)
回答你的实际问题......
枚举时,您将获得一个绑定到列表状态的IEnumerator,就像您要求它一样。进一步的操作在枚举器上运行(MoveNext,Current)。
当使用for循环时,如果通过索引获取特定项目的调用,则会生成序列。没有外部上下文,例如枚举器,它知道你在循环中。对于所有收藏品都知道,你只需要一件物品。由于该集合从未发布过调查员,因此无法知道您要求项目0,然后是项目1,然后是项目2等的原因是因为您正在查看列表。
如果你在走路的同时搞乱列表,那么无论如何都会出错。如果添加项目,那么for循环可以静默跳过一些,而foreach循环将抛出。如果删除项目,那么如果你运气不好,那么for循环可能会使索引超出范围,但大部分时间都可能会有效。
但我认为你理解这一切,你的问题只是为什么两种迭代方式表现不同。答案就是当你在一个案例中调用GetEnumerator时,以及在另一个案例中调用get_Item时,集合的状态(对于集合)是已知的。
答案 2 :(得分:1)
列表更改后,枚举器变为无效。如果您在列举列表时更改列表,则需要重新考虑一下策略。
在开始显示功能时获取一个新的枚举器,并在此过程中锁定列表。或者,将List的深层副本复制到新的_displayCollection列表中并通过此单独的集合进行枚举,除了在显示过程开始之前填充之外,该集合将不会被写入。希望这会有所帮助。
答案 3 :(得分:1)
不同之处在于,当您说“循环整个集合”时,您实际上并没有循环遍历集合,而是在1到50之间迭代整数,并在这些索引处添加集合。这对1到50之间的数字仍然存在的事实没有影响。
当您枚举列表时,您枚举的是项目,而不是索引。因此,在枚举时添加项目时,会使枚举失效。它的构建方式可以防止你正在做的事情,你可以在列表6中插入一个项目的同时枚举列表中的第6项,在那里你可以枚举旧项目或新项目,或者一些未定义的状态。
如果你想这样做,请寻找“线程安全”列表,但要准备好同时处理read + write的不准确性:)
答案 4 :(得分:1)
该列表有一个内部版本计数器,当您更改列表内容时会更新该计数器。枚举器跟踪版本并在看到列表已更改时抛出异常。
当您只是循环列表时,没有任何内容可以跟踪版本,因此没有任何内容可以捕获列表已更改。
如果在循环播放时更改列表,则可能会产生不需要的效果,这是枚举器保护您的效果。例如,如果您从列表中删除项目而不更改循环索引以使其仍指向同一项目,则可能会错过循环中的项目。同样,如果您在不更正索引的情况下插入项目,则可以多次迭代同一项目。
答案 5 :(得分:0)
您无法通过它来更改集合。
问题是,当你的收藏品不完整时,你开始枚举,并尝试在枚举时保持添加物品
答案 6 :(得分:-1)
代码存在缺陷,因为您正在睡眠5秒钟,但并非所有项目都已添加到列表中。这意味着在第一个线程完成向列表添加项目之前,您开始在一个线程上显示项目,从而导致基础集合变为chnage并使枚举器无效。
从添加代码中删除Thread.Sleep会突出显示:
public static void AddItems(object state_)
{
for (int i = 1; i <= 50; i++)
{
_collection.Add(i.ToString());
Console.WriteLine("Adding " + i);
}
}
而不是睡觉你应该使用等待第一个线程的同步机制来完成添加项目的工作。