it-swarm.com.de

Sammlung wurde geändert; Enumerationsoperation wird möglicherweise nicht ausgeführt

Ich kann diesem Fehler nicht auf den Grund gehen, denn wenn der Debugger angeschlossen ist, scheint er nicht aufzutreten. Unten ist der Code.

Dies ist ein WCF-Server in einem Windows-Dienst. Die Methode NotifySubscribers wird vom Dienst immer dann aufgerufen, wenn ein Datenereignis auftritt (in zufälligen Abständen, jedoch nicht sehr häufig - etwa 800 Mal pro Tag).

Wenn ein Windows Forms-Client abonniert, wird die Abonnenten-ID dem Abonnentenwörterbuch hinzugefügt. Wenn der Client die Abonnements kündigt, wird er aus dem Wörterbuch gelöscht. Der Fehler tritt auf, wenn (oder nach) ein Client abbestellt wird. Es scheint, dass die foreach () - Schleife beim nächsten Aufruf der NotifySubscribers () - Methode mit dem Fehler in der Betreffzeile fehlschlägt. Die Methode schreibt den Fehler in das Anwendungsprotokoll, wie im folgenden Code dargestellt. Wenn ein Debugger angeschlossen ist und ein Client die Abonnements abbestellt, wird der Code ordnungsgemäß ausgeführt.

Sehen Sie ein Problem mit diesem Code? Muss ich das Wörterbuch threadsicher machen?

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

Es ist wahrscheinlich, dass SignalData während der Schleife indirekt das Abonnentenwörterbuch unter der Haube ändert und zu dieser Nachricht führt. Sie können dies durch Ändern überprüfen 

foreach(Subscriber s in subscribers.Values)

Zu 

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

Wenn ich recht habe, wird das Problem verschwinden

Wenn Sie subscribers.Values.ToList () aufrufen, werden die Werte von subscribers.Values ​​in eine separate Liste am Anfang von foreach kopiert. Nichts anderes hat Zugriff auf diese Liste (es hat nicht einmal einen Variablennamen!), Es kann also nichts innerhalb der Schleife geändert werden.

1382
JaredPar

Wenn ein Abonnent sich abmeldet, ändern Sie den Inhalt der Abonnentensammlung während der Aufzählung.

Es gibt mehrere Möglichkeiten, dies zu beheben. Eine Möglichkeit besteht darin, die for-Schleife zu ändern, um eine explizite .ToList() zu verwenden:

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

Ein effizienterer Weg ist meiner Meinung nach, eine andere Liste zu haben, in der Sie erklären, dass Sie alles, was "entfernt werden soll", einfügt. Nachdem Sie Ihre Hauptschleife (ohne .ToList ()) beendet haben, führen Sie eine weitere Schleife über der Liste "zu entfernende Objekte" aus und entfernen dabei jeden Eintrag. In Ihrer Klasse fügen Sie also hinzu:

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

Dann ändern Sie es 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 );
}

Dies wird nicht nur Ihr Problem lösen, sondern auch verhindern, dass Sie eine Liste aus Ihrem Wörterbuch erstellen müssen. Dies ist teuer, wenn viele Abonnenten vorhanden sind. Angenommen, die Liste der Abonnenten, die bei einer bestimmten Iteration entfernt werden sollen, ist niedriger als die Gesamtanzahl in der Liste. Dies sollte schneller sein. Fühlen Sie sich jedoch frei, Ihr Profil zu erstellen, um sicher zu sein, dass dies der Fall ist, wenn Ihre spezifische Nutzungssituation in Frage steht.

55
x4000

Sie können das Abonnentenwörterbuch auch sperren, um zu verhindern, dass es bei jeder Schleife geändert wird:

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

Warum dieser Fehler?

Im Allgemeinen unterstützen .Net-Sammlungen nicht die gleichzeitige Aufzählung und Änderung. Wenn Sie versuchen, die Auflistungsliste während der Aufzählung zu ändern, wird eine Ausnahme ausgelöst. Das Problem hinter diesem Fehler ist also, dass wir die Liste/das Wörterbuch nicht ändern können, während wir dasselbe durchlaufen. 

Eine der Lösungen

Wenn wir ein Wörterbuch mithilfe einer Liste seiner Schlüssel durchlaufen, können wir das Wörterbuchobjekt parallel ändern, da wir durch die Schlüsselsammlung und Nicht das Wörterbuch (und seine Schlüsselsammlung) durchlaufen.

Beispiel

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

Hier ist ein Blogpost über diese Lösung.

Und für einen tiefen Tauchgang in StackOverflow: Warum tritt dieser Fehler auf?

12
open and free

Tatsächlich scheint mir das Problem, dass Sie Elemente aus der Liste entfernen und erwarten, die Liste weiterhin zu lesen, als wäre nichts geschehen.

Was Sie wirklich tun müssen, ist, vom Ende bis zum Anfang zu beginnen. Auch wenn Sie Elemente aus der Liste entfernen, können Sie sie weiter lesen.

4
luc.rg.roy

InvalidOperationException- Eine InvalidOperationException ist aufgetreten. Es berichtet, dass eine "Sammlung wurde geändert" in einer Foreach-Schleife

Verwenden Sie die break-Anweisung, sobald das Objekt entfernt wurde.

ex: 

ArrayList list = new ArrayList(); 

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

Ich habe viele Optionen dafür gesehen, aber für mich war dies die beste.

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

Dann blättern Sie einfach durch die Sammlung. 

Beachten Sie, dass eine ListItemCollection Duplikate enthalten kann. Standardmäßig gibt es nichts dagegen, dass Duplikate zur Sammlung hinzugefügt werden. Um Duplikate zu vermeiden, können Sie Folgendes tun:

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

Ich hatte das gleiche Problem und es wurde gelöst, als ich eine for-Schleife anstelle von foreach verwendete.

// 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);
}
2
Daniel Moreshet

Okay, was mir geholfen hat, ist rückwärts zu iterieren. Ich habe versucht, einen Eintrag aus einer Liste zu entfernen, aber durchläuft den Wert nach oben und die Schleife ist fehlerhaft, weil der Eintrag nicht mehr vorhanden war: 

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

                            myList.RemoveAt(x);

                        }
1
Mark Aven

Sie können das Wörterbuchobjekt der Abonnenten in denselben Typ eines temporären Wörterbuchobjekts kopieren und dann das temporäre Wörterbuchobjekt mithilfe der foreach-Schleife wiederholen. 

0
Rezoan

Es gibt einen Link, wo es sehr gut ausgearbeitet wurde, und die Lösung ist ebenfalls angegeben. Versuchen Sie es, wenn Sie die richtige Lösung erhalten haben, schreiben Sie bitte hier, damit die anderen verstehen können .. Die gegebene Lösung ist in Ordnung, dann kann der Post diese Lösung ausprobieren .

für Sie referenzieren Sie den ursprünglichen Link: -  https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

Wenn Sie .Net-Serialisierungsklassen verwenden, um ein Objekt zu serialisieren, dessen Definition einen Enumerable-Typ enthält, dh Collection, erhalten Sie leicht InvalidOperationException mit der Angabe "Collection wurde geändert. unter Multithread-Szenarien ..__ Die niedrigste Ursache ist, dass die Serialisierungsklassen die Auflistung über den Enumerator durchlaufen. In diesem Fall wird das Problem mit

Als erste Lösung können wir lock einfach als Synchronisationslösung verwenden, um sicherzustellen, dass die Operation mit dem List-Objekt nur jeweils von einem Thread aus ausgeführt werden kann. Wenn Sie eine Auflistung dieses Objekts serialisieren möchten, erhalten Sie eine Leistungsstrafe, wenn . Für jedes Objekt wird die Sperre angewendet.

Nun, .NET 4.0, das den Umgang mit Multithreading-Szenarien praktisch macht. Für dieses serialisierende Collection-Feldproblem habe ich festgestellt, dass wir nur die ConcurrentQueue (Check MSDN) -Klasse nutzen können, die eine thread-sichere und FIFO -Sammlung ist und Code sperrungsfrei macht.

Bei Verwendung dieser Klasse wird der Collection-Typ in seiner Einfachheit durch den Inhalt ersetzt, den Sie für Ihren Code ändern müssen.... Verwenden Sie Enqueue, um ein Element am Ende von ConcurrentQueue hinzuzufügen, und entfernen Sie den Sperrcode. Wenn für das Szenario, an dem Sie gerade arbeiten, Sammelobjekte wie List erforderlich sind, benötigen Sie noch ein paar weitere Codes, um ConcurrentQueue in Ihre Felder einzufügen.

Übrigens, ConcurrentQueue verfügt nicht über eine Clear-Methode aufgrund des zugrunde liegenden Algorithmus, der das atomare Löschen der Sammlung nicht zulässt. Wenn Sie es also selbst tun müssen, erstellen Sie am schnellsten eine neue leere ConcurrentQueue für einen Ersatz.

0
Meghraj

Eine andere Möglichkeit, dieses Problem zu lösen, besteht darin, anstelle der Elemente ein neues Wörterbuch zu erstellen und nur die Elemente hinzuzufügen, die nicht entfernt werden sollen, und das ursprüngliche Wörterbuch durch das neue zu ersetzen. Ich denke nicht, dass dies ein zu großes Effizienzproblem ist, weil dadurch die Anzahl der Wiederholungen der Struktur nicht erhöht wird.

0
ford prefect