Domanda

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.

Riscontri 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);
        }
    }
}
È stato utile?

Soluzione

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

A

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

Se ho ragione, il problema sparirà

Chiamando abbonati.Valori.ToList () copia i valori degli abbonati.Valore in un elenco separato all'inizio della ricerca. Nient'altro ha accesso a questo elenco (non ha nemmeno un nome di variabile!), Quindi nulla può modificarlo all'interno del ciclo.

Altri suggerimenti

Quando un abbonato annulla l'iscrizione, si sta modificando il contenuto della raccolta di Abbonati durante l'enumerazione.

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

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

Un modo più efficiente, secondo me, è quello di avere un altro elenco in cui dichiari di mettere tutto ciò che deve essere "rimosso". in. Quindi dopo aver terminato il ciclo principale (senza .ToList ()), esegui un altro ciclo sopra " da rimuovere " elenco, 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, ma 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 in ogni data iterazione sia inferiore al numero totale nell'elenco, questo dovrebbe essere più veloce. Ma ovviamente sentiti libero di profilarlo per essere sicuro che sia il caso in caso di dubbi sulla tua specifica situazione d'uso.

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

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 ripetiamo un dizionario usando un elenco delle sue chiavi, in parallelo possiamo modificare l'oggetto dizionario, mentre stiamo ripetendo la raccolta di chiavi e non il dizionario (e iterando la sua raccolta di 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 < em> post di blog su questa soluzione.

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

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, potrai continuare a leggerlo.

InvalidOperationException - Si è verificata una InvalidOperationException. Riporta una raccolta "quotata 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;
    }
}

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

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, scorrere semplicemente 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 farlo:

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

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

                        }

È 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.

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 eccessivo perché non aumenta il numero di volte in cui si scorre la struttura.

C'è un link in cui è stato elaborato molto bene & amp; 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, quindi altri possono 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, ad es. raccolta, otterrai facilmente InvalidOperationException dicendo che la raccolta è stata modificata; l'operazione di enumerazione non può eseguire " dove la tua codifica è in scenari multi-thread. La causa principale è che le classi di serializzazione eseguiranno l'iterazione tramite raccolta tramite enumeratore, come tale, il problema è provare a scorrere una raccolta mentre la modifica.

Prima soluzione, possiamo semplicemente usare il blocco come soluzione di sincronizzazione per assicurarlo l'operazione sull'oggetto Elenco può essere eseguita da un solo thread alla volta.  Ovviamente, questo sarà penalizzato dalle prestazioni se si desidera serializzare una raccolta di quell'oggetto, per ciascuno 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 thread-safe e FIFO e rende il codice privo di blocchi.

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.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top