Почему в некоторых случаях не работает цикл foreach?

Я использовал цикл 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, и это разбило репутацию, но, увы.

Павел Минаев - следующий заслуженный, поэтому он получает признание.

Спасибо за помощь людям добрым. :)


person Fake Code Monkey Rashid    schedule 22.07.2009    source источник
comment
Предоставьте несколько верхних кадров (т.е. из самого FCL) полученного вами исключения ArgumentException.   -  person Pavel Minaev    schedule 22.07.2009
comment
Цикл for работает, потому что вам повезло. Ваша логика потоковой передачи полностью нарушена, поэтому это может произойти случайно. Перейдя на цикл for, вы тонко изменили время выполнения некоторой операции, которая находится в состоянии гонки и больше не вызывает проблемы; что угодно могло заставить его вернуться. Если вы хотите иметь коллекцию, которая читается и изменяется в нескольких потоках, вам нужно быть очень-очень осторожными в отношении правильной блокировки. В противном случае он просто выйдет из строя случайным образом, как вы уже испытали. Рассмотрите возможность использования блокировки чтения-записи.   -  person Eric Lippert    schedule 22.07.2009
comment
@ Эрик Липперт: Почему это комментарий, а не ответ? Это полезно.   -  person Fake Code Monkey Rashid    schedule 22.07.2009
comment
В чем причина блокировки? В каком контексте? ASP.NET? Есть ли другие потоки, обращающиеся к этим данным?   -  person John Saunders    schedule 22.07.2009
comment
@eric Я предполагаю, что сейчас не время. цикл for будет перебирать все элементы в списке только в том случае, если ни один элемент не будет удален (т.е. в списке нет элементов). Если один будет удален, следующий в списке не будет посещен, поэтому в целом будет удалена только половина элементов. В то время как цикл foreach будет перезапускать итерацию каждый раз, когда элемент удаляется   -  person Rune FS    schedule 23.07.2009
comment
Я добавил в свое решение код, который делает то, что, как я думаю, вы пытаетесь достичь - непрерывную фоновую обработку запросов, которые добавляются в какую-то очередь.   -  person Thorarin    schedule 23.07.2009


Ответы (9)


Ваша проблема в том, что конструктор List<T>, который создает новый список из IEnumerable (который вы называете), не является потокобезопасным в отношении своего аргумента. Что происходит, пока это:

 new List<string>(Foo.Requests)

выполняется, другой поток изменяет Foo.Requests. Вам придется заблокировать его на время этого звонка.

[РЕДАКТИРОВАТЬ]

Как указал Эрик, другая проблема List<T> не гарантирует безопасность чтения читателями, пока другой поток ее изменяет. Т.е. одновременные читатели - это нормально, но одновременные читатели и писатели - нет. И хотя вы блокируете записи друг против друга, вы не блокируете чтение и запись.

person Pavel Minaev    schedule 22.07.2009

Ваша схема блокировки нарушена. Вам нужно заблокировать Foo.Requests() на всю продолжительность цикла, а не только при удалении элемента. В противном случае элемент может стать недействительным в середине вашей операции «обработки данных», и перечисление может измениться между переходами от элемента к элементу. И это предполагает, что вам не нужно вставлять коллекцию в течение этого интервала. Если это так, вам действительно нужно повторно фактор, чтобы использовать правильную очередь производителя / потребителя.

person Joel Coehoorn    schedule 22.07.2009
comment
Перечисление не изменится, потому что он делает копию коллекции специально для перечисления. И Remove не будет жаловаться, если он передал элемент, которого нет в коллекции (например, потому что другой поток уже удалил его). - person Pavel Minaev; 22.07.2009
comment
Возможно, это не то, что вам нужно. Обработка данных потенциально может занять много времени, в то время как новые данные добавляются асинхронно. Опять же, это будет означать, что после завершения цикла еще есть данные, которые нужно обработать, поскольку он создает неглубокую копию Foo.Requests. - person Thorarin; 22.07.2009
comment
@Pavel: Он копирует ссылки. Я больше говорю о другом потоке, удаляющем (и, возможно, также обрабатывающем) упомянутый объект из базовой коллекции запросов. Он мог закончить двойной работой. Или нить может вставить посередине, и он может что-то упустить. - person Joel Coehoorn; 22.07.2009

Увидев ваше исключение; мне кажется, что 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> 0.
person Thorarin    schedule 22.07.2009
comment
Некоторые люди выступают за блокировку всего цикла foreach. Меня учили, что блокировка должна быть как можно короче (как и то, что здесь делается. Насколько жизнеспособен этот подход? Будет ли он работать с несколькими потоками? - person Fake Code Monkey Rashid; 23.07.2009
comment
Это сработает, но то, что лучше всего в вашей ситуации, зависит от ряда вещей. Как долго длится обработка? Если вы установите блокировку вокруг цикла, процесс, добавляющий запрос, будет заблокирован, пока запросы обрабатываются. Я полагаю, это не то, что вам нужно, потому что в этом случае было бы намного проще использовать однопоточное решение. - person Thorarin; 23.07.2009
comment
Это смутно то, что я делаю, если свести его к псевдокоду, но это не совсем то, что я делаю. Это подход, о котором я не думал, и у меня есть чему поучиться. Конечно, он не будет работать с несколькими потоками как есть, но, тем не менее, это все равно интересно, поэтому спасибо за то, что поделились. :) - person Fake Code Monkey Rashid; 23.07.2009

Честно говоря, я бы предложил провести рефакторинг. Вы удаляете элементы из объекта, одновременно выполняя итерацию над ним. Ваш цикл может выйти до того, как вы обработаете все элементы.

person Polaris878    schedule 22.07.2009
comment
Если бы он удалял элементы из источника, который перебирает, он бы каждый раз столкнулся бы с очень большим исключением. Здесь дело обстоит не так. - person Thorarin; 22.07.2009
comment
Однако вопрос касается foreach. Но я согласен с вашим предложением по рефакторингу :) - person Thorarin; 22.07.2009

Три вещи:
- Я бы не блокировал их внутри оператора for (each), но за его пределами.
- Я бы не блокировал фактическую коллекцию, а локальный статический объект
- Вы не может изменить список / коллекцию, которую вы перечисляете

Для получения дополнительной информации проверьте:
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);
}
person Zyphrax    schedule 22.07.2009
comment
Он не изменяет перечисляемую коллекцию. Он сделал копию. - person Pavel Minaev; 22.07.2009
comment
Во втором примере (код для кода) он явно модифицирует существующую коллекцию. Не копия. - person Zyphrax; 22.07.2009
comment
Да, но это не приведет к сбою цикла for (хотя он может дать неверные результаты, если вы его не учитываете). Его вопрос: почему foreach терпит неудачу. - person Pavel Minaev; 22.07.2009
comment
Если вы заблокируете внутреннюю часть foreach, список не будет полностью заблокирован. Это могло измениться с каждым циклом-циклом. - person Zyphrax; 22.07.2009

Проблема в выражении

new List<string>(Foo.Requests)

внутри вашего foreach, потому что он не заблокирован. Я предполагаю, что пока .NET копирует вашу коллекцию запросов в новый список, список изменяется другим потоком.

person chris166    schedule 22.07.2009

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, должны участвовать в схеме блокировки.

person Amy B    schedule 22.07.2009

Вы пытаетесь удалить объекты из списка, когда выполняете итерацию по списку. (Хорошо, технически вы этого не делаете, но это цель, которую вы пытаетесь достичь).

Вот как вы это делаете правильно: во время итерации создайте еще один список записей, которые вы хотите удалить. Просто создайте другой (временный) список, поместите все записи, которые вы хотите удалить из исходного списка, во временный список.

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

Использование метода removeAll класса List.

person Valters Vingolds    schedule 22.07.2009

Я знаю, что это не то, о чем вы просили, но просто ради моего рассудка, следующее представляет собой намерение вашего кода:

private object _locker = new object();

// ...

lock (_locker) {
    Foo.Requests.Clear();
}
person John Saunders    schedule 22.07.2009
comment
Хм, нет. Я не думаю, что это все. Я получаю данные и помещаю их в список для обработки. Даже пока указанный список обрабатывается, данные все еще добавляются в него. Так что ни при каких обстоятельствах я не хочу просто очищать весь список. - person Fake Code Monkey Rashid; 23.07.2009
comment
Понятно. Я упустил важность вашего комментария обработки данных. - person John Saunders; 23.07.2009