为什么foreach循环在某些情况下不起作用?

时间:2009-07-22 18:44:45

标签: c# exception foreach for-loop

我正在使用foreach循环来处理要处理的数据列表(一旦处理就删除了所述数据 - 这是在锁内)。此方法偶尔会导致ArgumentException。

抓住它本来就很贵,所以我试着追查这个问题,但我无法理解。

我已经切换到for循环,问题似乎已经消失了。有人能解释发生了什么吗?即使有异常消息,我也不太了解幕后发生的事情。

为什么for循环显然有效?我是否设置了foreach循环错误或什么?

这几乎是我的循环设置方式:

foreach (string data in new List<string>(Foo.Requests))
{
    // Process the data.

    lock (Foo.Requests)
    {
        Foo.Requests.Remove(data);
    }
}

for (int i = 0; i < Foo.Requests.Count; i++)
{
    string data = Foo.Requests[i];

    // Process the data.

    lock (Foo.Requests)
    {
        Foo.Requests.Remove(data);
    }
}

编辑:for *循环是这样的设置,如下所示:

while (running)
{
    // [...]
}

编辑:根据要求添加了有关异常的更多信息。

System.ArgumentException: Destination array was not long enough. Check destIndex and length, and the array's lower bounds
  at System.Array.Copy (System.Array sourceArray, Int32 sourceIndex, System.Array destinationArray, Int32 destinationIndex, Int32 length) [0x00000] 
  at System.Collections.Generic.List`1[System.String].CopyTo (System.String[] array, Int32 arrayIndex) [0x00000] 
  at System.Collections.Generic.List`1[System.String].AddCollection (ICollection`1 collection) [0x00000] 
  at System.Collections.Generic.List`1[System.String]..ctor (IEnumerable`1 collection) [0x00000]

编辑:锁定的原因是有另一个线程添加数据。此外,最终,多个线程将处理数据(因此,如果整个设置错误,请提供建议)。

编辑:很难找到一个好的答案。

我发现埃里克·利珀特的评论值得,但他并没有真正回答(无论如何都投了他的评论)。

帕维尔·米娜耶夫,乔尔·科霍恩和索拉林都给出了我喜欢和投票的答案。索拉林还需要额外的20分钟来编写一些有用的代码。

我可以接受所有3并且让它分享声誉但是唉。

Pavel Minaev是下一个应得的人,所以他获得了荣誉。

感谢帮助好人。 :)

9 个答案:

答案 0 :(得分:13)

您的问题是List<T>的构造函数从IEnumerable创建一个新列表(这就是您所调用的)对于其参数而言不是线程安全的。会发生什么事情是这样的:

 new List<string>(Foo.Requests)

正在执行,另一个线程更改Foo.Requests。你必须在通话期间锁定它。

[编辑]

正如Eric所指出的那样,另一个问题List<T>并不能保证读者在另一个线程正在改变它时更安全。即并发读者是好的,但并发读者和作者不是。当你把你的写作相互锁定时,你不会把你的读取锁定在你的写入上。

答案 1 :(得分:5)

您的锁定方案已损坏。您需要在整个循环期间锁定Foo.Requests(),而不仅仅是在删除项目时。否则,在“处理数据”操作过程中,该项可能会变为无效,并且枚举可能会在从一个项目移动到另一个项目之间发生变化。这假设您不需要在此间隔期间插入集合。如果是这种情况,您确实需要重新考虑使用适当的生产者/消费者队列。

答案 2 :(得分:5)

看到你的异常后;它看起来我正在构建浅拷贝时改变Foo.Requests。把它改成这样的东西:

List<string> requests;

lock (Foo.Requests)
{
    requests = new List<string>(Foo.Requests);
}

foreach (string data in requests)
{
    // Process the data.

    lock (Foo.Requests)
    {
        Foo.Requests.Remove(data);
    }
}

不是问题,但是......

话虽如此,我有点怀疑以上是你想要的。如果在处理期间有新请求进入,则当foreach循环终止时,它们将不会被处理。因为我很无聊,所以我认为你正在尝试实现这一点:

class RequestProcessingThread
{
    // Used to signal this thread when there is new work to be done
    private AutoResetEvent _processingNeeded = new AutoResetEvent(true);

    // Used for request to terminate processing
    private ManualResetEvent _stopProcessing = new ManualResetEvent(false);

    // Signalled when thread has stopped processing
    private AutoResetEvent _processingStopped = new AutoResetEvent(false);

    /// <summary>
    /// Called to start processing
    /// </summary>
    public void Start()
    {
        _stopProcessing.Reset();

        Thread thread = new Thread(ProcessRequests);
        thread.Start();
    }

    /// <summary>
    /// Called to request a graceful shutdown of the processing thread
    /// </summary>
    public void Stop()
    {
        _stopProcessing.Set();

        // Optionally wait for thread to terminate here
        _processingStopped.WaitOne();
    }

    /// <summary>
    /// This method does the actual work
    /// </summary>
    private void ProcessRequests()
    {
        WaitHandle[] waitHandles = new WaitHandle[] { _processingNeeded, _stopProcessing };

        Foo.RequestAdded += OnRequestAdded;

        while (true)
        {
            while (Foo.Requests.Count > 0)
            {
                string request;
                lock (Foo.Requests)
                {
                    request = Foo.Requests.Peek();
                }

                // Process request
                Debug.WriteLine(request);

                lock (Foo.Requests)
                {
                    Foo.Requests.Dequeue();
                }
            }

            if (WaitHandle.WaitAny(waitHandles) == 1)
            {
                // _stopProcessing was signalled, exit the loop
                break;
            }
        }

        Foo.RequestAdded -= ProcessRequests;

        _processingStopped.Set();
    }

    /// <summary>
    /// This method will be called when a new requests gets added to the queue
    /// </summary>
    private void OnRequestAdded()
    {
        _processingNeeded.Set();
    }
}


static class Foo
{
    public delegate void RequestAddedHandler();
    public static event RequestAddedHandler RequestAdded;

    static Foo()
    {
        Requests = new Queue<string>();
    }

    public static Queue<string> Requests
    {
        get;
        private set;
    }

    public static void AddRequest(string request)
    {
        lock (Requests)
        {
            Requests.Enqueue(request);
        }

        if (RequestAdded != null)
        {
            RequestAdded();
        }
    }
}

这还有一些问题,我将留给读者:

  • 每次处理请求后都应该检查_stopProcessing
  • 如果有多个线程正在处理
  • ,Peek()/ Dequeue()方法将无法工作
  • 封装不足:可以访问Foo.Requests,但如果您希望处理任何请求,则需要使用Foo.AddRequest添加任何请求。
  • 在多个处理线程的情况下:需要处理循环内空的队列,因为Count&gt;周围没有锁定。 0检查。

答案 3 :(得分:2)

三件事:
- 我不会把它们锁在for(每个)声明中,但不在它之外 - 我不会锁定实际的集合,而是锁定本地静态对象
- 您无法修改您要枚举的列表/集合

有关更多信息,请检查:
http://msdn.microsoft.com/en-us/library/c5kehkcz(VS.80).aspx

lock (lockObject) {
   foreach (string data in new List<string>(Foo.Requests))
        Foo.Requests.Remove(data);
}

答案 4 :(得分:2)

说实话,我建议重构一下。您正在迭代该对象,同时也会迭代该对象。在处理完所有项目之前,你的循环实际上可以退出。

答案 5 :(得分:2)

问题在于表达式

new List<string>(Foo.Requests)
在你的foreach里面,因为它没有锁定。我假设当.NET将您的请求集合复制到新列表时,该列表会被另一个线程修改

答案 6 :(得分:1)

foreach (string data in new List<string>(Foo.Requests))
{
    // Process the data.
    lock (Foo.Requests)
    {
        Foo.Requests.Remove(data);
    }
}

假设您有两个执行此代码的线程。

  

在System.Collections.Generic.List1 [System.String] .. ctor

  • Thread1开始处理列表。
  • Thread2调用List构造函数,该构造函数计算要创建的数组的计数。
  • Thread1更改列表中的项目数。
  • Thread2的项目数量错误。

您的锁定方案有误。在for循环示例中甚至是错误的。

每次访问共享资源时都需要锁定 - 甚至是读取或复制它。这并不意味着您需要锁定整个操作。这意味着共享此共享资源的每个人都需要参与锁定方案。

还要考虑防御性复制:

List<string> todos = null;
List<string> empty = new List<string>();
lock(Foo.Requests)
{
  todos = Foo.Requests;
  Foo.Requests = empty;
}

//now process local list todos

即便如此,所有共享Foo.Requests的人都必须参与锁定方案。

答案 7 :(得分:0)

您正在尝试从迭代列表中删除列表中的对象。 (好的,从技术上讲,你不是这样做的,但这是你想要实现的目标)。

以下是您正确执行的操作:迭代时,构建另一个要删除的条目列表。只需构造另一个(临时)列表,将要从原始列表中删除的所有条目放入临时列表。

List entries_to_remove = new List(...);

foreach( entry in original_list ) {
   if( entry.someCondition() == true ) { 
      entries_to_remove.add( entry );
   }
}

// Then when done iterating do: 
original_list.removeAll( entries_to_remove );

使用List类的“removeAll”方法。

答案 8 :(得分:0)

我知道这不是你要求的,但仅仅是为了我自己的理智,以下内容代表了你的代码的意图:

private object _locker = new object();

// ...

lock (_locker) {
    Foo.Requests.Clear();
}