线程和IEnumerable; “集合被修改” - 例外

时间:2011-07-17 21:15:41

标签: c# multithreading linq

想象一下,在下面的类中,一个线程获取IEnumerable对象并开始迭代元素。在迭代过程中,另一个线程出现并通过Add-method向library_entries添加一个新条目。是否会在迭代中抛出“集合被修改”的异常?或者锁定会阻止添加元素,直到迭代完成?或者都不是?

谢谢!

public static class Library
{
    private static List<string> library_entries = new List<string>(1000000);

    public static void Add(string entry)
    {
        lock (library_entries)
            library_entries.Add(entry);
    }

    public static IEnumerable<string> GetEntries()
    {
        return library_entries.Where(entry => !string.IsNullOrEmpty(entry));
    }
}

4 个答案:

答案 0 :(得分:4)

不,你不会得到例外,你正在使用Linq查询。更糟糕的是,它将无法预测地失败。最典型的结果是同一项被枚举两次,但是当List在Add()调用期间重新分配其内部存储时,包括IndexOutOfRangeException,任何事情都是可能的。每周一次,给予或接受。

调用GetEntries()并使用枚举器的代码也必须获取锁。锁定Where()表达式不够好。除非您创建列表的副本。

答案 1 :(得分:1)

锁定根本没有帮助,因为迭代不使用锁定。我建议重写GetEntries()函数以返回副本。

public static IEnumerable<string> GetEntries()
{
    lock(lockObj)
    {
        return library_entries.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
    }
}

注意这会返回一致的快照。即迭代时它不会返回新添加的对象。

我更喜欢锁定一个私有对象,它的唯一目的是锁定,但由于列表是私有的,所以它不是真正的问题,只是一个风格问题。

您也可以编写自己的迭代器:

int i=0;
bool MoveNext()
{
  lock(lockObj)
  {
      if(i<list.Count)
          return list[i];
      i++;
  }
}

如果这是一个好主意取决于您的访问模式,锁争用,列表大小,......您可能还想使用读写锁来避免许多读访问的争用。

答案 2 :(得分:0)

静态GetEntries方法不会对静态library_entries集合=&gt;执行任何锁定它不是线程安全的,并且来自多个线程的任何并发调用都可能会中断。您已锁定Add方法的事实很好,但枚举不是线程安全操作,因此如果您打算同时调用GetEntries方法,则必须锁定它。此外,因为此方法返回IEnumerable<T>,所以在您开始枚举哪个可能超出GetEntries方法之前,它不会对实际列表执行任何操作。因此,您可以在LINQ链的末尾添加.ToList()调用,然后锁定整个操作。

答案 3 :(得分:0)

是的,会抛出异常 - 您没有锁定常见对象。此外,GetEntries方法中的锁定将无效,因为该调用立即返回。迭代时会发生锁定。