简体   繁体   English

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

[英]Why doesn't a foreach loop work in certain cases?

I was using a foreach loop to go through a list of data to process (removing said data once processed--this was inside a lock). 我正在使用foreach循环来处理要处理的数据列表(一旦处理就删除了所述数据 - 这是在锁内)。 This method caused an ArgumentException now and then. 此方法偶尔会导致ArgumentException。

Catching it would have been expensive so I tried tracking down the issue but I couldn't figure it out. 抓住它本来是昂贵的,所以我试图追查这个问题,但我无法弄明白。

I have since switched to a for loop and the problem seems to have went away. 我已经切换到for循环,问题似乎已经消失了。 Can someone explain what happened? 有人能解释发生了什么吗? Even with the exception message I don't quite understand what took place behind the scenes. 即使有异常消息,我也不太了解幕后发生了什么。

Why is the for loop apparently working? 为什么for循环显然有效? Did I set up the foreach loop wrong or what? 我是否设置了foreach循环错误或什么?

This is pretty much how my loops were set up: 这几乎是我的循环设置方式:

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

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

and

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

    // Process the data.

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

EDIT: The for* loop is in a while setup like so: 编辑:for *循环是这样的设置,如下所示:

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

EDIT: Added more information about the exception as requested. 编辑:根据要求添加了有关异常的更多信息。

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]

EDIT: The reason for the locking is that there is another thread adding data. 编辑:锁定的原因是有另一个线程添加数据。 Also, eventually, more than one thread will be processing data (so if the entire setup is wrong, please advise). 此外,最终,多个线程将处理数据(因此,如果整个设置错误,请告知)。

EDIT: It was hard to pick a good answer. 编辑:很难找到一个好的答案。

I found Eric Lippert's comment deserving but he didn't really answer (up-voted his comment anyhow). 我发现埃里克·利珀特的评论值得,但他并没有真正回答(无论如何都投了他的评论)。

Pavel Minaev, Joel Coehoorn and Thorarin all gave answers I liked and up-voted. Pavel Minaev,Joel Coehoorn和Thorarin都给出了我喜欢和投票的答案。 Thorarin also took an extra 20 minutes to write some helpful code. 索拉林还花了20分钟写了一些有用的代码。

I which I could accept all 3 and have it split the reputation but alas. 我可以接受所有这三个并让它分享声誉但唉。

Pavel Minaev is the next deserving so he gets the credit. Pavel Minaev是下一个应得的人,所以他获得了荣誉。

Thanks for the help good people. 感谢帮助好人。 :) :)

Your problem is that the constructor of List<T> that creates a new list from IEnumerable (which is what you call) isn't thread-safe with respect to its argument. 你的问题是List<T>的构造函数从IEnumerable (你调用的)创建一个新的列表,它的参数不是线程安全的。 What happens is that while this: 会发生什么事情,虽然这个:

 new List<string>(Foo.Requests)

is executing, another thread changes Foo.Requests . 正在执行,另一个线程更改Foo.Requests You'll have to lock it for the duration of that call. 你必须在通话期间锁定它。

[EDIT] [编辑]

As pointed out by Eric, another problem List<T> isn't guaranteed safe for readers to read while another thread is changing it, either. 正如Eric指出的那样,另一个问题是List<T>并不能保证读者在另一个线程正在改变它时更安全。 Ie concurrent readers are okay, but concurrent reader and writer are not. 即并发读者是好的,但并发读者和作者不是。 And while you lock your writes against each other, you don't lock your reads against your writes. 当您将写入锁定为彼此时,您不会将读取锁定在写入之上。

Your locking scheme is broken. 您的锁定方案已被破坏。 You need to lock Foo.Requests() for the entire duration of the loop, not just when removing an item. 您需要在整个循环期间锁定Foo.Requests() ,而不仅仅是在删除项目时。 Otherwise the item might become invalid in the middle of your "process the data" operation and enumeration might change in between moving from item to item. 否则,在“处理数据”操作过程中,该项可能会变为无效,并且枚举可能会在从一个项目移动到另一个项目之间发生变化。 And that assumes you don't need to insert the collection during this interval as well. 这假设您不需要在此间隔期间插入集合。 If that's the case, you really need to re-factor to use a proper producer/consumer queue. 如果是这种情况,您确实需要重新考虑使用适当的生产者/消费者队列。

After seeing your exception; 看到你的例外后; it looks to me that Foo.Requests is being changed while the shallow copy is being constructed. 它看起来我正在构建浅拷贝时改变Foo.Requests。 Change it to something like this: 将其更改为以下内容:

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);
    }
}

Not the question, but... 不是问题,但......

That being said, I somewhat doubt the above is what you want either. 话虽如此,我有点怀疑上面是你想要的。 If new requests are coming in during processing, they will not have been processed when your foreach loop terminates. 如果在处理期间有新请求进入,则当foreach循环终止时,它们将不会被处理。 Since I was bored, here's something along the lines that I think you're trying to achieve: 由于我很无聊,这里有一些我认为你想要实现的东西:

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();
        }
    }
}

There are still a few problems with this, which I will leave to the reader: 这还有一些问题,我将留给读者:

  • Checking for _stopProcessing should probably be done after every time a request is processed 每次处理请求后都应该检查_stopProcessing
  • The Peek() / Dequeue() approach won't work if you have multiple threads doing processing 如果有多个线程在进行处理,Peek()/ Dequeue()方法将不起作用
  • Insufficient encapsulation: Foo.Requests is accessible, but Foo.AddRequest needs to be used to add any requests if you want them processed. 封装不足:Foo.Requests是可访问的,但如果您希望处理任何请求,则需要使用Foo.AddRequest添加任何请求。
  • In case of multiple processing threads: need to handle the queue being empty inside the loop, since there is no lock around the Count > 0 check. 在多个处理线程的情况下:需要在循环内处理队列为空,因为Count> 0检查周围没有锁定。

Three things: 三件事:
- I wouldn't put them lock within the for(each) statement, but outside of it. - 我不会把它们锁在for(每个)语句中,但是在它之外。
- I wouldn't lock the actual collection, but a local static object - 我不会锁定实际的集合,而是一个本地静态对象
- You can not modify a list/collection that you're enumerating - 您无法修改您枚举的列表/集合

For more information check: 有关更多信息,请检查
http://msdn.microsoft.com/en-us/library/c5kehkcz(VS.80).aspx 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);
}

To be completely honest, I would suggest refactoring that. 说实话,我建议重构一下。 You are removing items from the object while also iterating over that. 您正在迭代该对象,同时也会迭代该对象。 Your loop could actually exit before you've processed all items. 在处理完所有项目之前,您的循环实际上可能会退出。

The problem is the expression 问题在于表达方式

new List<string>(Foo.Requests)

inside your foreach, because it's not under a lock. 在你的foreach里面,因为它没有锁定。 I assume that while .NET copies your requests collection into a new list, the list is modified by another thread 我假设当.NET将您的请求集合复制到新列表中时,该列表会被另一个线程修改

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

Suppose you have two threads executing this code. 假设您有两个执行此代码的线程。

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

  • Thread1 starts processing the list. Thread1开始处理列表。
  • Thread2 calls the List constructor, which takes a count for the array to be created. Thread2调用List构造函数,该构造函数计算要创建的数组的计数。
  • Thread1 changes the number of items in the list. Thread1更改列表中的项目数。
  • Thread2 has the wrong number of items. Thread2的项目数量错误。

Your locking scheme is wrong. 你的锁定方案是错误的。 It's even wrong in the for loop example. 在for循环示例中甚至是错误的。

You need to lock every time you access the shared resource - even to read or copy it. 每次访问共享资源时都需要锁定 - 甚至是读取或复制它。 This doesn't mean you need to lock for the whole operation. 这并不意味着您需要锁定整个操作。 It does mean that everyone sharing this shared resource needs to participate in the locking scheme. 这意味着共享此共享资源的每个人都需要参与锁定方案。

Also consider defensive copying: 还要考虑防御性复制:

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

//now process local list todos

Even so, all those that share Foo.Requests must participate in the locking scheme. 即便如此,所有共享Foo.Requests的人都必须参与锁定方案。

You are trying to remove objects from list as you are iterating through list. 在迭代列表时,您正尝试从列表中删除对象。 (OK, technically, you are not doing this, but that's the goal you are trying to achieve). (好的,从技术上讲,你不是这样做的,但这是你想要达到的目标)。

Here's how you do it properly: while iterating, construct another list of entries that you want to remove. 以下是您正确执行的操作:迭代时,构建另一个要删除的条目列表。 Simply construct another (temp) list, put all entries you want to remove from original list into the temp list. 只需构造另一个(临时)列表,将要从原始列表中删除的所有条目放入临时列表中。

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 );

Using "removeAll" method of List class. 使用List类的“removeAll”方法。

I know it's not what you asked for, but just for the sake of my own sanity, does the following represent the intention of your code: 我知道这不是你要求的,但仅仅是为了我自己的理智,下面代表你的代码的意图:

private object _locker = new object();

// ...

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM