La raccolta è stata modificata; l'operazione di enumerazione potrebbe non essere eseguita


911

Non riesco ad arrivare in fondo a questo errore, perché quando il debugger è collegato, sembra che non si verifichi. Di seguito è riportato il codice.

Questo è un server WCF in un servizio Windows. Il metodo NotifySubscribers viene chiamato dal servizio ogni volta che si verifica un evento di dati (a intervalli casuali, ma non molto spesso - circa 800 volte al giorno).

Quando un client Windows Form si abbona, l'ID abbonato viene aggiunto al dizionario degli abbonati e quando il client annulla l'iscrizione, viene eliminato dal dizionario. L'errore si verifica quando (o dopo) un client annulla l'iscrizione. Sembra che alla successiva chiamata del metodo NotifySubscribers (), il ciclo foreach () fallisce con l'errore nella riga dell'oggetto. Il metodo scrive l'errore nel registro dell'applicazione come mostrato nel codice seguente. Quando un debugger è collegato e un client annulla l'iscrizione, il codice viene eseguito correttamente.

Vedi un problema con questo codice? Devo rendere il dizionario thread-safe?

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

nel mio caso è stato un effetto collaterale perché stavo usando alcuni .Include ("tabella") che sono stati modificati durante il processo - non molto ovvio durante la lettura del codice. tuttavia sono stato fortunato che quelle Include non fossero necessarie (sì! vecchio codice non mantenuto) e ho risolto il mio problema semplicemente rimuovendole
Adi

Risposte:


1631

Ciò che sta probabilmente accadendo è che SignalData sta cambiando indirettamente il dizionario degli abbonati sotto il cofano durante il ciclo e portando a quel messaggio. Puoi verificarlo cambiando

foreach(Subscriber s in subscribers.Values)

Per

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

Se ho ragione, il problema sparirà

La chiamata subscribers.Values.ToList()copia i valori di subscribers.Valuesin un elenco separato all'inizio di foreach. Nient'altro ha accesso a questo elenco (non ha nemmeno un nome variabile!), Quindi nulla può modificarlo all'interno del ciclo.


14
BTW .ToList () è presente nella dll System.Core che non è compatibile con le applicazioni .NET 2.0. Quindi potrebbe essere necessario modificare l'app di destinazione in .Net 3.5
mishal153

60
Non capisco perché hai fatto una ToList e perché questo risolve tutto
PositiveGuy

204
@CoffeeAddict: il problema è che subscribers.Valuesviene modificato all'interno del foreachciclo. La chiamata subscribers.Values.ToList()copia i valori di subscribers.Valuesin un elenco separato all'inizio di foreach. Nient'altro ha accesso a questo elenco (non ha nemmeno un nome variabile!) , Quindi nulla può modificarlo all'interno del ciclo.
BlueRaja - Danny Pflughoeft,

31
Si noti che ToListpotrebbe anche essere generato se la raccolta è stata modificata durante l' ToListesecuzione.
Sriram Sakthivel,

16
Sono abbastanza sicuro che non risolva il problema, ma rende semplicemente più difficile la riproduzione. ToListnon è un'operazione atomica. Ciò che è ancora più divertente, ToListfondamentalmente fa il proprio foreachinternamente per copiare elementi in una nuova istanza di elenco, il che significa che hai risolto un foreachproblema aggiungendo foreachun'iterazione aggiuntiva (anche se più veloce) .
Groo,

113

Quando un abbonato annulla la sottoscrizione, si sta modificando il contenuto della raccolta di Sottoscrittori durante l'enumerazione.

Esistono diversi modi per risolvere questo problema, uno sta cambiando il ciclo for per usare un esplicito .ToList():

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...

64

Un modo più efficiente, secondo me, è quello di avere un altro elenco in cui dichiari di mettere tutto ciò che deve essere "rimosso". Quindi, dopo aver terminato il ciclo principale (senza .ToList ()), esegui un altro ciclo sull'elenco "da rimuovere", rimuovendo ogni voce in tempo reale. Quindi nella tua classe aggiungi:

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

Quindi lo cambi in:

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

Questo non solo risolverà il tuo problema, ti impedirà di continuare a creare un elenco dal tuo dizionario, che è costoso se ci sono molti abbonati. Supponendo che l'elenco di abbonati da rimuovere su ogni data iterazione sia inferiore al numero totale nell'elenco, questo dovrebbe essere più veloce. Ma naturalmente sentiti libero di profilarlo per essere sicuro che sia il caso in caso di dubbi nella tua specifica situazione d'uso.


9
Mi aspetto che valga la pena considerare che se ne hai uno, stai lavorando con collezioni più grandi. Se fosse piccolo probabilmente avrei semplicemente ToList e andare avanti.
Karl Kieninger,

42

Puoi anche bloccare il dizionario dei tuoi iscritti per impedirne la modifica ogni volta che viene eseguito il ciclo:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }

2
È un esempio completo? Ho una classe (_dictionary obj sotto) che contiene un dizionario generico <string, int> chiamato MarkerFrequencies, ma farlo non ha risolto istantaneamente il crash: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> pair in _dictionary.MarkerFrequencies) {...}}
Jon Coombs

4
@JCoombs è possibile che si stia modificando, eventualmente riassegnando il MarkerFrequenciesdizionario all'interno del blocco stesso, il che significa che l'istanza originale non è più bloccata. Prova anche a usare a foranziché a foreach, guarda questo e questo . Fammi sapere se questo lo risolve.
Mohammad Sepahvand,

1
Bene, finalmente sono riuscito a risolvere questo errore e questi suggerimenti sono stati utili - grazie! Il mio set di dati era piccolo e questa operazione era solo un aggiornamento del display dell'interfaccia utente, quindi l'iterazione su una copia sembrava la migliore. (Ho anche provato a utilizzare for anziché anziché foreach dopo aver bloccato MarkerFrequencies in entrambi i thread. Ciò ha impedito l'arresto anomalo ma sembrava richiedere ulteriore lavoro di debug. E ha introdotto una maggiore complessità. La mia unica ragione per avere due thread qui è consentire all'utente di annullare un'operazione, a proposito. L'interfaccia utente non modifica direttamente quei dati.)
Jon Coombs

9
Il problema è che, per le applicazioni di grandi dimensioni, i blocchi possono essere un grande successo per le prestazioni: meglio usare una raccolta nello System.Collections.Concurrentspazio dei nomi.
BrainSlugs83

28

Perché questo errore?

In generale, le raccolte .Net non supportano l'enumerazione e la modifica contemporaneamente. Se si tenta di modificare l'elenco di raccolte durante l'enumerazione, viene generata un'eccezione. Quindi il problema dietro questo errore è che non possiamo modificare la lista / il dizionario mentre stiamo eseguendo lo stesso ciclo.

Una delle soluzioni

Se eseguiamo l'iterazione di un dizionario utilizzando un elenco delle sue chiavi, in parallelo possiamo modificare l'oggetto dizionario, poiché stiamo ripetendo la raccolta chiavi e non il dizionario (ripetendo la raccolta chiavi).

Esempio

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

Ecco un post sul blog su questa soluzione.

E per un'immersione profonda in StackOverflow: perché si verifica questo errore?


Grazie! Una chiara spiegazione del perché accada e mi ha dato immediatamente il modo di risolverlo nella mia applicazione.
Kim Crosser,

5

In realtà il problema mi sembra che stai rimuovendo elementi dall'elenco e ti aspetti di continuare a leggere l'elenco come se nulla fosse successo.

Quello che devi veramente fare è iniziare dalla fine e tornare all'inizio. Anche se rimuovi elementi dall'elenco sarai in grado di continuare a leggerlo.


Non vedo come ciò farebbe la differenza? Se si rimuove un elemento in mezzo all'elenco, si genererebbe comunque un errore in quanto l'elemento a cui si sta tentando di accedere non è stato trovato?
Zapnologica,

4
@Zapnologica la differenza è - non dovresti enumerare l'elenco - invece di fare un for / each, faresti un for / next e accedervi per intero - puoi sicuramente modificare un elenco a in un per / ciclo successivo, ma mai in un ciclo per / ogni (perché per / ogni enumera ) - puoi anche farlo andando avanti in un per / prossimo, a condizione che tu abbia una logica aggiuntiva per regolare i tuoi contatori, ecc.
BrainSlugs83

4

InvalidOperationException - Si è verificata una InvalidOperationException. Riporta una "raccolta modificata" in un ciclo foreach

Usa istruzione break, una volta rimosso l'oggetto.

ex:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}

Soluzione valida e semplice se sai che la tua lista deve essere rimossa al massimo da un elemento.
Tawab Wakil,

3

Ho avuto lo stesso problema ed è stato risolto quando ho usato un forloop invece di 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);
}

11
Questo codice è errato e salterà alcuni elementi nella raccolta se qualsiasi elemento verrà rimosso. Ad esempio: hai var arr = ["a", "b", "c"] e nella prima iterazione (i = 0) rimuovi l'elemento in posizione 0 (elemento "a"). Successivamente, tutti gli elementi dell'array si sposteranno di una posizione verso l'alto e l'array sarà ["b", "c"]. Quindi, nella prossima iterazione (i = 1) controllerai l'elemento nella posizione 1 che sarà "c" e "b". Questo è sbagliato. Per risolvere il problema, devi spostarti dal basso verso l'alto
Kaspars Ozols il

3

Okay, quindi quello che mi ha aiutato è stato iterare all'indietro. Stavo cercando di rimuovere una voce da un elenco ma iterando verso l'alto e ha rovinato il ciclo perché la voce non esisteva più:

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

                            myList.RemoveAt(x);

                        }

2

Ho visto molte opzioni per questo, ma per me questo è stato il migliore.

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

Quindi semplicemente scorrere attraverso la raccolta.

Tenere presente che un oggetto ListItemCollection può contenere duplicati. Per impostazione predefinita, non c'è nulla che impedisca l'aggiunta di duplicati alla raccolta. Per evitare duplicati puoi fare questo:

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

In che modo questo codice impedirebbe l'inserimento di duplicati nel database. Ho scritto qualcosa di simile e quando aggiungo nuovi utenti dalla casella di riepilogo, se ne tengo accidentalmente selezionato uno che è già nell'elenco, verrà creata una voce duplicata. Hai qualche suggerimento su @Mike?
Jamie

1

La risposta accettata è imprecisa e errata nel caso peggiore. Se vengono apportate modifiche duranteToList() , è comunque possibile che si verifichi un errore. Inoltre lock, quali prestazioni e sicurezza dei thread devono essere prese in considerazione se si dispone di un membro pubblico, una soluzione adeguata può utilizzare tipi immutabili .

In generale, un tipo immutabile significa che non è possibile modificarne lo stato una volta creato. Quindi il tuo codice dovrebbe apparire come:

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
        }
    }
    //.........
}

Questo può essere particolarmente utile se hai un membro pubblico. Altre classi possono sempre foreachsui tipi immutabili senza preoccuparsi della modifica della raccolta.


1

Ecco uno scenario specifico che richiede un approccio specializzato:

  1. Il Dictionary viene enumerato frequentemente.
  2. Il Dictionaryviene modificato di rado.

In questo scenario, creare una copia del Dictionary(o del Dictionary.Values) prima di ogni enumerazione può essere piuttosto costoso. La mia idea sulla risoluzione di questo problema è quella di riutilizzare la stessa copia memorizzata nella cache in più enumerazioni e osservare una IEnumeratordelle Dictionaryeccezioni dell'originale . L'enumeratore verrà memorizzato nella cache insieme ai dati copiati e verrà interrogato prima di iniziare una nuova enumerazione. In caso di eccezione, la copia memorizzata nella cache verrà eliminata e ne verrà creata una nuova. Ecco la mia implementazione di questa idea:

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

Esempio di utilizzo:

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)
{
    //...
}

Sfortunatamente questa idea non può essere attualmente utilizzata con la classe Dictionaryin .NET Core 3.0, poiché questa classe non genera un'eccezione Collection è stata modificata quando elencata, i metodi Removee Clearvengono invocati. Tutti gli altri contenitori che ho controllato si comportano in modo coerente. Ho controllato sistematicamente queste classi: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V>e SortedDictionary<T,V>. Solo i due metodi summenzionati della Dictionaryclasse in .NET Core non invalidano l'enumerazione.


Aggiornamento: ho risolto il problema sopra riportato confrontando anche le lunghezze della collezione memorizzata nella cache e originale. Questa correzione presuppone che il dizionario verrà passato direttamente come argomento al EnumerableSnapshotcostruttore di 's, e la sua identità non sarà nascosto per (ad esempio) una proiezione come: dictionary.Select(e => e).ΤοEnumerableSnapshot().


Importante: la classe precedente non è thread-safe. È inteso per essere utilizzato dal codice eseguito esclusivamente in un singolo thread.


0

È possibile copiare l'oggetto dizionario dei sottoscrittori su uno stesso tipo di oggetto dizionario temporaneo e quindi iterare l'oggetto dizionario temporaneo utilizzando il ciclo foreach.


(Questo post non sembra fornire una risposta di qualità alla domanda. Modifica la tua risposta e, o semplicemente pubblicala come commento alla domanda).
sɐunıɔ ןɐ qɐp,

0

Quindi un modo diverso di risolvere questo problema sarebbe invece di rimuovere gli elementi creare un nuovo dizionario e aggiungere solo gli elementi che non si desidera rimuovere, quindi sostituire il dizionario originale con quello nuovo. Non penso che questo sia un problema di efficienza perché non aumenta il numero di volte in cui si scorre la struttura.


0

C'è un link in cui è stato elaborato molto bene e viene anche fornita una soluzione. Provalo se hai la soluzione corretta, per favore pubblica qui in modo che altri possano capire. La soluzione fornita è ok quindi come il post in modo che altri possano provare queste soluzioni.

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

Quando utilizziamo le classi di serializzazione .Net per serializzare un oggetto in cui la sua definizione contiene un tipo Enumerable, ovvero collection, otterrai facilmente InvalidOperationException che dice "La raccolta è stata modificata; l'operazione di enumerazione potrebbe non essere eseguita" laddove la tua codifica si trova in scenari multi-thread. La causa principale è che le classi di serializzazione eseguiranno l'iterazione attraverso la raccolta tramite l'enumeratore, pertanto il problema va a provare a scorrere un'insieme durante la modifica.

Prima soluzione, possiamo semplicemente usare il blocco come soluzione di sincronizzazione per garantire che l'operazione sull'oggetto Elenco possa essere eseguita da un solo thread alla volta. Ovviamente, riceverai una penalità per le prestazioni che, se vuoi serializzare una raccolta di quell'oggetto, per ognuno di essi verrà applicato il blocco.

Bene, .Net 4.0 che rende utile la gestione di scenari multi-thread. per questo problema sul campo della raccolta in serie, ho scoperto che possiamo trarre vantaggio dalla classe ConcurrentQueue (Verifica MSDN), che è una raccolta FIFO e thread-safe e rende il blocco del codice libero.

Usando questa classe, nella sua semplicità, le cose che devi modificare per il tuo codice stanno sostituendo il tipo Collection con esso, usa Enqueue per aggiungere un elemento alla fine di ConcurrentQueue, rimuovi quel codice di blocco. Oppure, se lo scenario su cui stai lavorando richiede elementi di raccolta come Elenco, avrai bisogno di qualche codice in più per adattare ConcurrentQueue nei tuoi campi.

A proposito, ConcurrentQueue non ha un metodo Clear a causa dell'algoritmo sottostante che non consente la cancellazione atomica della raccolta. quindi devi farlo da solo, il modo più veloce è ricreare una nuova ConcurrentQueue vuota per una sostituzione.


-1

Di recente mi sono imbattuto personalmente in questo codice sconosciuto che si trovava in un dbcontext aperto con Linq's .Remove (item). Mi sono divertito un mondo a localizzare il debug dell'errore perché gli articoli sono stati rimossi la prima volta che sono stati ripetuti, e se ho provato a fare un passo indietro e a ripetere il passaggio, la raccolta era vuota, dato che ero nello stesso contesto!

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.