Kolekcja została zmodyfikowana; Operacja wyliczenia może nie zostać wykonana

909

Nie mogę dotrzeć do sedna tego błędu, ponieważ kiedy dołączony jest debugger, wydaje się, że nie występuje. Poniżej znajduje się kod.

To jest serwer WCF w usłudze Windows. Metoda NotifySubscribers jest wywoływana przez usługę za każdym razem, gdy występuje zdarzenie danych (w przypadkowych odstępach czasu, ale niezbyt często - około 800 razy dziennie).

Gdy klient Windows Forms subskrybuje, identyfikator subskrybenta jest dodawany do słownika subskrybentów, a gdy klient anuluje subskrypcję, jest usuwany ze słownika. Błąd występuje, gdy klient anuluje subskrypcję (lub później). Wygląda na to, że przy następnym wywołaniu metody NotifySubscribers () pętla foreach () zawiedzie z błędem w temacie. Metoda zapisuje błąd w dzienniku aplikacji, jak pokazano w poniższym kodzie. Gdy podłączony jest debuger, a klient anuluje subskrypcję, kod działa poprawnie.

Czy widzisz problem z tym kodem? Czy muszę zapewnić bezpieczeństwo wątku w słowniku?

[ServiceBehavior(InstanceContextMode=InstanceContextMode.Single)]
public class SubscriptionServer : ISubscriptionServer
{
    private static IDictionary<Guid, Subscriber> subscribers;

    public SubscriptionServer()
    {            
        subscribers = new Dictionary<Guid, Subscriber>();
    }

    public void NotifySubscribers(DataRecord sr)
    {
        foreach(Subscriber s in subscribers.Values)
        {
            try
            {
                s.Callback.SignalData(sr);
            }
            catch (Exception e)
            {
                DCS.WriteToApplicationLog(e.Message, 
                  System.Diagnostics.EventLogEntryType.Error);

                UnsubscribeEvent(s.ClientId);
            }
        }
    }


    public Guid SubscribeEvent(string clientDescription)
    {
        Subscriber subscriber = new Subscriber();
        subscriber.Callback = OperationContext.Current.
                GetCallbackChannel<IDCSCallback>();

        subscribers.Add(subscriber.ClientId, subscriber);

        return subscriber.ClientId;
    }


    public void UnsubscribeEvent(Guid clientId)
    {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                    e.Message);
        }
    }
}
Cdonner
źródło
w moim przypadku był to efekt uboczny, ponieważ korzystałem z kilku .Include („tabeli”), które zostały zmodyfikowane podczas procesu - niezbyt oczywiste podczas czytania kodu. Miałem jednak szczęście, że te elementy nie były potrzebne (tak! stary, nieobsługiwany kod) i rozwiązałem problem, usuwając je
Adi

Odpowiedzi:

1630

Prawdopodobnie dzieje się tak, że SignalData pośrednio zmienia słownik subskrybentów pod maską podczas pętli i prowadzi do tego komunikatu. Możesz to sprawdzić, zmieniając

foreach(Subscriber s in subscribers.Values)

Do

foreach(Subscriber s in subscribers.Values.ToList())

Jeśli mam rację, problem zniknie

Wywołanie powoduje subscribers.Values.ToList()skopiowanie wartości subscribers.Valuesdo osobnej listy na początku foreach. Nic innego nie ma dostępu do tej listy (nie ma nawet nazwy zmiennej!), Więc nic nie może go zmodyfikować w pętli.

JaredPar
źródło
14
BTW .ToList () jest obecny w dll System.Core, który nie jest kompatybilny z aplikacjami .NET 2.0. Może być więc konieczna zmiana aplikacji docelowej na .Net 3.5
mishal153
60
Nie rozumiem, dlaczego stworzyłeś ToList i dlaczego to naprawia wszystko
PositiveGuy
204
@CoffeeAddict: Problem polega na tym, że subscribers.Valuesjest modyfikowany w foreachpętli. Wywołanie powoduje subscribers.Values.ToList()skopiowanie wartości subscribers.Valuesdo osobnej listy na początku foreach. Nic innego nie ma dostępu do tej listy (nie ma nawet nazwy zmiennej!) , Więc nic nie może go zmodyfikować w pętli.
BlueRaja - Danny Pflughoeft
31
Zauważ, że ToListmoże również rzucać, jeśli kolekcja została zmodyfikowana podczas ToListwykonywania.
Sriram Sakthivel
16
Jestem pewien, że myśl nie rozwiązuje problemu, a jedynie utrudnia reprodukcję. ToListnie jest operacją atomową. Co jest jeszcze zabawniejsze, w ToListzasadzie robi to foreachwewnętrznie, aby kopiować elementy do nowej instancji listy, co oznacza, że ​​rozwiązałeś foreachproblem, dodając dodatkową (choć szybszą) foreachiterację.
Groo
113

Gdy subskrybent zrezygnuje z subskrypcji, zmieniasz zawartość kolekcji subskrybentów podczas wyliczania.

Istnieje kilka sposobów rozwiązania tego problemu, jednym z nich jest zmiana pętli for na użycie jawnego .ToList():

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
Mitch Pszenica
źródło
64

Moim zdaniem bardziej skutecznym sposobem jest utworzenie kolejnej listy, w której zadeklarujesz, że umieszczasz w niej wszystko, co można „usunąć”. Następnie po zakończeniu głównej pętli (bez .ToList ()) wykonujesz kolejną pętlę na liście „do usunięcia”, usuwając każdą pozycję w miarę upływu czasu. W swojej klasie dodajesz:

private List<Guid> toBeRemoved = new List<Guid>();

Następnie zmień na:

public void NotifySubscribers(DataRecord sr)
{
    toBeRemoved.Clear();

    ...your unchanged code skipped...

   foreach ( Guid clientId in toBeRemoved )
   {
        try
        {
            subscribers.Remove(clientId);
        }
        catch(Exception e)
        {
            System.Diagnostics.Debug.WriteLine("Unsubscribe Error " + 
                e.Message);
        }
   }
}

...your unchanged code skipped...

public void UnsubscribeEvent(Guid clientId)
{
    toBeRemoved.Add( clientId );
}

To nie tylko rozwiąże twój problem, ale również nie pozwoli ci tworzyć listy ze słownika, co jest kosztowne, jeśli jest tam wielu subskrybentów. Zakładając, że lista subskrybentów do usunięcia w dowolnej iteracji jest niższa niż całkowita liczba na liście, powinno to być szybsze. Ale oczywiście możesz go profilować, aby mieć pewność, że tak jest w przypadku jakichkolwiek wątpliwości co do konkretnej sytuacji użytkowania.

x4000
źródło
9
Oczekuję, że będzie to warte rozważenia, jeśli masz model pracujący z większymi kolekcjami. Jeśli to małe, prawdopodobnie po prostu ToList i przejdźmy dalej.
Karl Kieninger
42

Możesz także zablokować słownik subskrybentów, aby zapobiec jego modyfikowaniu za każdym razem, gdy jest zapętlony:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
Mohammad Sepahvand
źródło
2
Czy to kompletny przykład? Mam klasę (_dictionary obj poniżej), która zawiera ogólny słownik <ciąg, int> o nazwie MarkerFrequencies, ale zrobienie tego nie rozwiązało natychmiast awarii: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> para w _dictionary.MarkerFrequencies) {...}}
Jon Coombs
4
@JCoombs jest możliwe, że modyfikujesz, prawdopodobnie ponownie przypisujesz MarkerFrequenciessłownik wewnątrz samej blokady, co oznacza, że ​​oryginalna instancja nie jest już zablokowana. Spróbuj także użyć forzamiast zamiast foreach, Zobacz to i to . Daj mi znać, jeśli to rozwiąże problem.
Mohammad Sepahvand
1
Cóż, w końcu udało mi się naprawić ten błąd, a te wskazówki były pomocne - dzięki! Mój zestaw danych był niewielki, a ta operacja była tylko aktualizacją ekranu interfejsu użytkownika, więc iteracja nad kopią wydawała się najlepsza. (Eksperymentowałem również z użyciem for zamiast zamiast foreach po zablokowaniu MarkerFrequencies w obu wątkach. To zapobiegło awarii, ale wydawało się, że wymaga dalszej pracy debugowania. I wprowadziło większą złożoność. Moim jedynym powodem posiadania dwóch wątków tutaj jest umożliwienie użytkownikowi anulowania tak na marginesie, operacja. Interfejs użytkownika nie modyfikuje bezpośrednio tych danych.)
Jon Coombs
9
Problem polega na tym, że w przypadku dużych aplikacji zamki mogą być dużym spadkiem wydajności - lepiej użyć kolekcji w System.Collections.Concurrentprzestrzeni nazw.
BrainSlugs83
28

Dlaczego ten błąd?

Zasadniczo kolekcje .Net nie obsługują jednoczesnego wyliczania i modyfikacji. Jeśli spróbujesz zmodyfikować listę kolekcji podczas wyliczania, pojawi się wyjątek. Problem polega na tym, że nie możemy modyfikować listy / słownika, gdy przechodzimy przez to samo.

Jedno z rozwiązań

Jeśli iterujemy słownik przy użyciu listy jego kluczy, równolegle możemy modyfikować obiekt słownika, ponieważ iterujemy przez kolekcję kluczy, a nie przez słownik (i iterację jego kolekcji kluczy).

Przykład

//get key collection from dictionary into a list to loop through
List<int> keys = new List<int>(Dictionary.Keys);

// iterating key collection using a simple for-each loop
foreach (int key in keys)
{
  // Now we can perform any modification with values of the dictionary.
  Dictionary[key] = Dictionary[key] - 1;
}

Oto post na blogu o tym rozwiązaniu.

I do głębokiego nurkowania w StackOverflow: Dlaczego ten błąd występuje?

otwarty i bezpłatny
źródło
Dziękuję Ci! Jasne wyjaśnienie, dlaczego tak się dzieje, i natychmiast dało mi sposób na rozwiązanie tego w mojej aplikacji.
Kim Crosser,
5

Wydaje mi się, że problem polega na tym, że usuwasz elementy z listy i spodziewasz się czytać listę tak, jakby nic się nie wydarzyło.

To, co naprawdę musisz zrobić, to zacząć od końca i wrócić do początku. Nawet jeśli usuniesz elementy z listy, będziesz mógł kontynuować czytanie.

luc.rg.roy
źródło
Nie rozumiem, jak to by miało jakąkolwiek różnicę? Jeśli usuniesz element na środku listy, nadal zgłasza błąd, ponieważ element, do którego próbuje uzyskać dostęp, nie może zostać znaleziony?
Zapnologica,
4
@Zapnologica różnica jest - nie byłoby wyliczanie listy - zamiast robi dla / każdy, byłbyś robi na / obok i dostępu do niego przez liczbę całkowitą - na pewno można zmodyfikować listę A w pętla for / next, ale nigdy w pętli for / each (ponieważ dla / each wylicza ) - możesz to również zrobić w przód dla for / next, pod warunkiem, że masz dodatkową logikę do dostosowania liczników itp.
BrainSlugs83
4

InvalidOperationException - Wystąpił wyjątek InvalidOperationException. Podaje, że „kolekcja została zmodyfikowana” w pętli foreach

Użyj instrukcji break, gdy obiekt zostanie usunięty.

dawny:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
vivek
źródło
Dobre i proste rozwiązanie, jeśli wiesz, że na liście znajduje się najwyżej jeden element do usunięcia.
Tawab Wakil
3

Miałem ten sam problem i został rozwiązany, gdy forzamiast niego użyłem pętli foreach.

// foreach (var item in itemsToBeLast)
for (int i = 0; i < itemsToBeLast.Count; i++)
{
    var matchingItem = itemsToBeLast.FirstOrDefault(item => item.Detach);

   if (matchingItem != null)
   {
      itemsToBeLast.Remove(matchingItem);
      continue;
   }
   allItems.Add(itemsToBeLast[i]);// (attachDetachItem);
}
Daniel Moreshet
źródło
11
Ten kod jest nieprawidłowy i pominie niektóre elementy w kolekcji, jeśli jakikolwiek element zostanie usunięty. Na przykład: masz var arr = [„a”, „b”, „c”], aw pierwszej iteracji (i = 0) usuwasz element w pozycji 0 (element „a”). Po tym wszystkie elementy tablicy przesuną się o jedną pozycję w górę, a tablica będzie [„b”, ​​„c”]. Zatem w następnej iteracji (i = 1) sprawdzisz element na pozycji 1, który będzie „c”, a nie „b”. To jest źle. Aby to naprawić, musisz przejść od dołu do góry
Kaspars Ozols
3

Ok, więc pomogło mi iterowanie wstecz. Próbowałem usunąć wpis z listy, ale iterując w górę, to zepsuło pętlę, ponieważ wpis już nie istniał:

for (int x = myList.Count - 1; x > -1; x--)
                        {

                            myList.RemoveAt(x);

                        }
Mark Aven
źródło
2

Widziałem wiele opcji, ale dla mnie ta była najlepsza.

ListItemCollection collection = new ListItemCollection();
        foreach (ListItem item in ListBox1.Items)
        {
            if (item.Selected)
                collection.Add(item);
        }

Następnie wystarczy przejrzeć kolekcję.

Pamiętaj, że ListItemCollection może zawierać duplikaty. Domyślnie nic nie stoi na przeszkodzie dodaniu duplikatów do kolekcji. Aby uniknąć duplikatów, możesz to zrobić:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }
Mikrofon
źródło
W jaki sposób ten kod zapobiega wprowadzaniu duplikatów do bazy danych. Napisałem coś podobnego i kiedy dodam nowych użytkowników z listy, jeśli przypadkowo zachowam zaznaczenie jednego, który jest już na liście, utworzy duplikat. Czy masz jakieś sugestie @Mike?
Jamie
1

W najgorszym przypadku przyjęta odpowiedź jest nieprecyzyjna i niepoprawna. Jeśli zmiany zostaną wprowadzone w trakcieToList() , nadal może wystąpić błąd. Poza tym lock, jaką wydajność i bezpieczeństwo wątków należy wziąć pod uwagę, jeśli masz członka publicznego, właściwym rozwiązaniem może być użycie niezmiennych typów .

Zasadniczo niezmienny typ oznacza, że ​​nie można zmienić jego stanu po utworzeniu. Twój kod powinien więc wyglądać następująco:

public class SubscriptionServer : ISubscriptionServer
{
    private static ImmutableDictionary<Guid, Subscriber> subscribers = ImmutableDictionary<Guid, Subscriber>.Empty;
    public void SubscribeEvent(string id)
    {
        subscribers = subscribers.Add(Guid.NewGuid(), new Subscriber());
    }
    public void NotifyEvent()
    {
        foreach(var sub in subscribers.Values)
        {
            //.....This is always safe
        }
    }
    //.........
}

Może to być szczególnie przydatne, jeśli masz członka publicznego. Inne klasy mogą zawsze foreachużywać niezmiennych typów bez obawy o modyfikację kolekcji.

Joe
źródło
1

Oto konkretny scenariusz, który uzasadnia specjalistyczne podejście:

  1. DictionaryJest wyliczone często.
  2. DictionaryJest modyfikowany rzadko.

W tym scenariuszu utworzenie kopii Dictionary(lub Dictionary.Values) przed każdym wyliczeniem może być dość kosztowne. Moim pomysłem na rozwiązanie tego problemu jest ponowne użycie tej samej kopii w pamięci podręcznej w wielu wyliczeniach i obejrzenie IEnumeratororyginału pod Dictionarykątem wyjątków. Moduł wyliczający zostanie zbuforowany wraz z skopiowanymi danymi i zapytany przed rozpoczęciem nowego wyliczenia. W przypadku wyjątku kopia w pamięci podręcznej zostanie odrzucona i zostanie utworzona nowa. Oto moja realizacja tego pomysłu:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

public class EnumerableSnapshot<T> : IEnumerable<T>, IDisposable
{
    private IEnumerable<T> _source;
    private IEnumerator<T> _enumerator;
    private ReadOnlyCollection<T> _cached;

    public EnumerableSnapshot(IEnumerable<T> source)
    {
        _source = source ?? throw new ArgumentNullException(nameof(source));
    }

    public IEnumerator<T> GetEnumerator()
    {
        if (_source == null) throw new ObjectDisposedException(this.GetType().Name);
        if (_enumerator == null)
        {
            _enumerator = _source.GetEnumerator();
            _cached = new ReadOnlyCollection<T>(_source.ToArray());
        }
        else
        {
            var modified = false;
            if (_source is ICollection collection) // C# 7 syntax
            {
                modified = _cached.Count != collection.Count;
            }
            if (!modified)
            {
                try
                {
                    _enumerator.MoveNext();
                }
                catch (InvalidOperationException)
                {
                    modified = true;
                }
            }
            if (modified)
            {
                _enumerator.Dispose();
                _enumerator = _source.GetEnumerator();
                _cached = new ReadOnlyCollection<T>(_source.ToArray());
            }
        }
        return _cached.GetEnumerator();
    }

    public void Dispose()
    {
        _enumerator?.Dispose();
        _enumerator = null;
        _cached = null;
        _source = null;
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public static class EnumerableSnapshotExtensions
{
    public static EnumerableSnapshot<T> ToEnumerableSnapshot<T>(
        this IEnumerable<T> source) => new EnumerableSnapshot<T>(source);
}

Przykład użycia:

private static IDictionary<Guid, Subscriber> _subscribers;
private static EnumerableSnapshot<Subscriber> _subscribersSnapshot;

//...(in the constructor)
_subscribers = new Dictionary<Guid, Subscriber>();
_subscribersSnapshot = _subscribers.Values.ToEnumerableSnapshot();

// ...(elsewere)
foreach (var subscriber in _subscribersSnapshot)
{
    //...
}

Niestety, tego pomysłu nie można obecnie zastosować z klasą Dictionaryw .NET Core 3.0, ponieważ ta klasa nie generuje wyjątku Collection został zmodyfikowany po wyliczeniu oraz metodach Removei Clearwywoływaniu. Wszystkie inne pojemniki, które sprawdziłem, zachowują się konsekwentnie. I systematycznie sprawdzane tych klas: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V>i SortedDictionary<T,V>. Tylko dwie wyżej wymienione metody Dictionaryklasy w .NET Core nie unieważniają wyliczenia.


Aktualizacja: Rozwiązałem powyższy problem, porównując również długości pamięci podręcznej i oryginalnej kolekcji. Poprawka zakłada, że słownik będzie przekazywane bezpośrednio, jako argument do EnumerableSnapshot„s konstruktora, a jego tożsamość nie będą ukryte przez (na przykład) do projekcji jak: dictionary.Select(e => e).ΤοEnumerableSnapshot().


Ważne: powyższa klasa nie jest bezpieczna dla wątków. Jest przeznaczony do użycia z kodu działającego wyłącznie w jednym wątku.

Theodor Zoulias
źródło
0

Możesz skopiować obiekt słownika subskrybentów do tego samego typu tymczasowego obiektu słownika, a następnie iterować tymczasowy obiekt słownika za pomocą pętli foreach.

Rezoan
źródło
(Wydaje się, że ten post nie zawiera wysokiej jakości odpowiedzi na pytanie. Edytuj odpowiedź i po prostu opublikuj ją jako komentarz do pytania).
sɐunıɔ ןɐ qɐp
0

Tak więc innym sposobem rozwiązania tego problemu byłoby usunięcie elementów, utworzenie nowego słownika i dodanie tylko tych elementów, których nie chcesz usunąć, a następnie zastąpienie oryginalnego słownika nowym. Nie sądzę, że jest to zbyt duży problem z wydajnością, ponieważ nie zwiększa liczby iteracji w strukturze.

ford prefekt
źródło
0

Jest jeden link, w którym opracowano go bardzo dobrze i podano również rozwiązanie. Wypróbuj, jeśli masz odpowiednie rozwiązanie, napisz tutaj, aby inni mogli to zrozumieć. Podane rozwiązanie jest w porządku, tak jak post, więc inni mogą wypróbować te rozwiązania.

do odniesienia oryginalny link: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

Kiedy używamy klas .Net Serialization do serializacji obiektu, którego definicja zawiera typ Enumerable, tj. Kolekcja, łatwo dostaniesz InvalidOperationException, mówiąc: „Kolekcja została zmodyfikowana; operacja wyliczenia może nie zostać wykonana” tam, gdzie kodowanie odbywa się w scenariuszach wielowątkowych. Podstawową przyczyną jest to, że klasy serializacji będą iterować przez kolekcję za pomocą modułu wyliczającego, dlatego problem polega na próbie iteracji przez kolekcję podczas jej modyfikowania.

Po pierwsze, możemy po prostu użyć blokady jako rozwiązania synchronizacji, aby zapewnić, że operacja na obiekcie List może być wykonywana tylko z jednego wątku na raz. Oczywiście otrzymasz karę wydajności, że jeśli chcesz serializować kolekcję tego obiektu, to dla każdego z nich zostanie zastosowana blokada.

Cóż, .Net 4.0, który ułatwia radzenie sobie ze scenariuszami wielowątkowymi. w przypadku tego problemu z serializowaniem pola kolekcji okazało się, że możemy po prostu skorzystać z klasy ConcurrentQueue (Check MSDN), która jest bezpieczna dla wątków i zawiera kolekcję FIFO i sprawia, że ​​kod nie jest blokowany.

Używając tej klasy, w swojej prostocie, rzeczy, które musisz zmodyfikować dla swojego kodu, zastępują go typem Kolekcji, użyj Enqueue, aby dodać element na końcu ConcurrentQueue, usuń kod blokady. Lub, jeśli scenariusz, nad którym pracujesz, nie wymaga gromadzenia rzeczy takich jak List, będziesz potrzebował jeszcze kilku kodów, aby dostosować ConcurrentQueue do swoich pól.

BTW, ConcurrentQueue nie ma metody Clear ze względu na podstawowy algorytm, który nie pozwala na atomowe czyszczenie kolekcji. więc musisz to zrobić sam, najszybszym sposobem jest odtworzenie nowego pustego ConcurrentQueue w celu wymiany.

użytkownik8851697
źródło
-1

Osobiście natknąłem się na to niedawno w nieznanym kodzie, gdzie był w otwartym kontakcie db z .Remove (pozycja) Linqa. Miałem piekło czasu, gdy lokalizowałem błąd debugowania, ponieważ elementy zostały usunięte przy pierwszej iteracji, a jeśli spróbowałem się cofnąć i ponownie, kolekcja była pusta, ponieważ byłem w tym samym kontekście!

Michał Sefranek
źródło