Ich kann diesem Fehler nicht auf den Grund gehen, denn wenn der Debugger angehängt 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 vorliegt (in zufälligen Intervallen, aber nicht sehr oft - ungefähr 800 Mal pro Tag).
Wenn ein Windows Forms-Client abonniert, wird die Abonnenten-ID zum Abonnentenwörterbuch hinzugefügt, und wenn der Client sich abmeldet, wird sie aus dem Wörterbuch gelöscht. Der Fehler tritt auf, wenn (oder nachdem) sich ein Client abmeldet. Es scheint, dass beim nächsten Aufruf der NotifySubscribers () -Methode die foreach () -Schleife mit dem Fehler in der Betreffzeile fehlschlägt. Die Methode schreibt den Fehler wie im folgenden Code gezeigt in das Anwendungsprotokoll. Wenn ein Debugger angehängt ist und ein Client sich abmeldet, 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);
}
}
}
quelle
Antworten:
Was wahrscheinlich passiert, ist, 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
Zu
Wenn ich recht habe, wird das Problem verschwinden
Beim Aufrufen werden
subscribers.Values.ToList()
die Werte vonsubscribers.Values
in eine separate Liste am Anfang des kopiertforeach
. Nichts anderes hat Zugriff auf diese Liste (sie hat nicht einmal einen Variablennamen!), Daher kann nichts sie innerhalb der Schleife ändern.quelle
subscribers.Values
es innerhalb derforeach
Schleife geändert wird. Beim Aufrufen werdensubscribers.Values.ToList()
die Werte vonsubscribers.Values
in eine separate Liste am Anfang des kopiertforeach
. Nichts anderes hat Zugriff auf diese Liste (sie hat nicht einmal einen Variablennamen!) , Daher kann nichts sie innerhalb der Schleife ändern.ToList
dies auch ausgelöst werden kann, wenn die Sammlung während derToList
Ausführung geändert wurde .ToList
ist keine atomare Operation. Was noch lustiger ist,ToList
macht sichforeach
intern grundsätzlich selbstständig, um Elemente in eine neue Listeninstanz zu kopieren. Dies bedeutet , dass Sie einforeach
Problem behoben haben , indem Sie eine zusätzliche (wenn auch schnellere)foreach
Iteration hinzugefügt haben .Wenn sich ein Abonnent abmeldet, ändern Sie während der Aufzählung den Inhalt der Abonnentensammlung.
Es gibt verschiedene Möglichkeiten, dies zu beheben. Eine davon besteht darin, die for-Schleife so zu ändern, dass eine explizite verwendet wird
.ToList()
:quelle
Meiner Meinung nach ist es effizienter, eine andere Liste zu erstellen, in die Sie alles einfügen, was "entfernt" werden soll. Nachdem Sie Ihre Hauptschleife beendet haben (ohne .ToList ()), führen Sie eine weitere Schleife über die Liste "Zu entfernen" durch und entfernen jeden Eintrag, sobald dies geschieht. Also fügst du in deiner Klasse hinzu:
Dann ändern Sie es in:
Dies löst nicht nur Ihr Problem, sondern verhindert auch, dass Sie ständig eine Liste aus Ihrem Wörterbuch erstellen müssen. Dies ist teuer, wenn sich viele Abonnenten dort befinden. Angenommen, die Liste der Abonnenten, die bei einer bestimmten Iteration entfernt werden sollen, ist niedriger als die Gesamtzahl in der Liste. Dies sollte schneller sein. Aber natürlich können Sie es auch profilieren, um sicherzugehen, dass dies der Fall ist, wenn Zweifel an Ihrer spezifischen Verwendungssituation bestehen.
quelle
Sie können das Abonnentenwörterbuch auch sperren, um zu verhindern, dass es bei jeder Schleife geändert wird:
quelle
MarkerFrequencies
Wörterbuch innerhalb der Sperre selbst ändern und möglicherweise neu zuweisen , was bedeutet, dass die ursprüngliche Instanz nicht mehr gesperrt ist. Versuchen Sie auch, afor
anstelle von a zu verwendenforeach
. Siehe dies und das . Lassen Sie mich wissen, ob das das Problem löst.System.Collections.Concurrent
Namespace zu verwenden.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 dieselbe durchlaufen.
Eine der Lösungen
Wenn wir ein Wörterbuch anhand einer Liste seiner Schlüssel iterieren, können wir parallel das Wörterbuchobjekt ändern, während wir die Schlüsselsammlung und nicht das Wörterbuch durchlaufen (und seine Schlüsselsammlung iterieren).
Beispiel
Hier ist ein Blog-Beitrag über diese Lösung.
Und für einen tiefen Tauchgang in StackOverflow: Warum tritt dieser Fehler auf?
quelle
Eigentlich scheint mir das Problem, dass Sie Elemente aus der Liste entfernen und erwarten, die Liste weiterhin zu lesen, als wäre nichts passiert.
Was Sie wirklich tun müssen, ist, vom Ende bis zum Anfang zu beginnen. Selbst wenn Sie Elemente aus der Liste entfernen, können Sie sie weiter lesen.
quelle
InvalidOperationException - Eine InvalidOperationException ist aufgetreten. Es wird berichtet, dass eine "Sammlung geändert wurde" in einer foreach-Schleife
Verwenden Sie die break-Anweisung, sobald das Objekt entfernt wurde.
Ex:
quelle
Ich hatte das gleiche Problem und es wurde gelöst, als ich
for
stattdessen eine Schleife verwendeteforeach
.quelle
Okay, was mir geholfen hat, war rückwärts zu iterieren. Ich habe versucht, einen Eintrag aus einer Liste zu entfernen, aber nach oben iteriert und die Schleife durcheinander gebracht, weil der Eintrag nicht mehr vorhanden war:
quelle
Ich habe viele Optionen dafür gesehen, aber für mich war diese die beste.
Dann durchlaufen Sie einfach die Sammlung.
Beachten Sie, dass eine ListItemCollection Duplikate enthalten kann. Standardmäßig verhindert nichts, dass der Sammlung Duplikate hinzugefügt werden. Um Duplikate zu vermeiden, können Sie Folgendes tun:
quelle
Die akzeptierte Antwort ist ungenau und im schlimmsten Fall falsch. Wenn während Änderungen vorgenommen werden
ToList()
, kann dennoch ein Fehler auftreten. Außerdemlock
, welche Leistung und Thread-Sicherheit berücksichtigt werden muss, wenn Sie ein öffentliches Mitglied haben, kann eine geeignete Lösung die Verwendung unveränderlicher Typen sein .Im Allgemeinen bedeutet ein unveränderlicher Typ, dass Sie den einmal erstellten Status nicht ändern können. Ihr Code sollte also so aussehen:
Dies kann besonders nützlich sein, wenn Sie ein öffentliches Mitglied haben. Andere Klassen können immer
foreach
unveränderliche Typen verwenden, ohne sich Gedanken über die Änderung der Sammlung machen zu müssen.quelle
Hier ist ein spezifisches Szenario, das einen speziellen Ansatz erfordert:
Dictionary
wird häufig aufgezählt.Dictionary
wird selten geändert.In diesem Szenario kann das Erstellen einer Kopie des
Dictionary
(oder desDictionary.Values
) vor jeder Aufzählung sehr kostspielig sein. Meine Idee zur Lösung dieses Problems ist es, dieselbe zwischengespeicherte Kopie in mehreren Aufzählungen wiederzuverwenden und einIEnumerator
OriginalDictionary
auf Ausnahmen zu prüfen. Der Enumerator wird zusammen mit den kopierten Daten zwischengespeichert und abgefragt, bevor eine neue Enumeration gestartet wird. Im Falle einer Ausnahme wird die zwischengespeicherte Kopie verworfen und eine neue erstellt. Hier ist meine Umsetzung dieser Idee:Anwendungsbeispiel:
Leider kann diese Idee derzeit nicht mit der Klasse
Dictionary
in .NET Core 3.0 verwendet werden, da diese Klasse keine Ausnahme auslöst, wenn die Auflistung geändert wurde und die MethodenRemove
undClear
aufgerufen werden. Alle anderen von mir überprüften Container verhalten sich konsistent. Ich habe systematisch diese Klassen:List<T>
,Collection<T>
,ObservableCollection<T>
,HashSet<T>
,SortedSet<T>
,Dictionary<T,V>
undSortedDictionary<T,V>
. Nur die beiden oben genannten Methoden derDictionary
Klasse in .NET Core machen die Aufzählung nicht ungültig.Update: Ich habe das obige Problem behoben, indem ich auch die Längen der zwischengespeicherten und der ursprünglichen Sammlung verglichen habe. Bei diesem Fix wird davon ausgegangen, dass das Wörterbuch direkt als Argument an den
EnumerableSnapshot
Konstruktor des 'übergeben wird und seine Identität nicht durch (zum Beispiel) eine Projektion wie: verborgen wirddictionary.Select(e => e).ΤοEnumerableSnapshot()
.Wichtig: Die obige Klasse ist nicht threadsicher. Es soll aus Code verwendet werden, der ausschließlich in einem einzelnen Thread ausgeführt wird.
quelle
Sie können das Abonnentenwörterbuchobjekt in denselben Typ eines temporären Wörterbuchobjekts kopieren und dann das temporäre Wörterbuchobjekt mithilfe der foreach-Schleife iterieren.
quelle
Eine andere Möglichkeit, dieses Problem zu lösen, besteht darin, anstelle des Entfernens der Elemente ein neues Wörterbuch zu erstellen und nur die Elemente hinzuzufügen, die Sie nicht entfernen möchten, und dann das ursprüngliche Wörterbuch durch das neue zu ersetzen. Ich denke nicht, dass dies ein zu großes Effizienzproblem ist, da es nicht die Häufigkeit erhöht, mit der Sie über die Struktur iterieren.
quelle
Es gibt einen Link, bei dem es sehr gut ausgearbeitet wurde und es wird auch eine Lösung angegeben. Probieren Sie es aus, wenn Sie die richtige Lösung gefunden haben. Bitte posten Sie hier, damit andere verstehen können. Die gegebene Lösung ist dann wie in der Post in Ordnung, damit andere diese Lösung ausprobieren können.
für Sie Referenz Original Link: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/
Wenn wir .Net-Serialisierungsklassen verwenden, um ein Objekt zu serialisieren, dessen Definition einen Enumerable-Typ enthält, dh eine Auflistung, wird leicht die InvalidOperationException mit der Meldung "Sammlung wurde geändert; Aufzählungsoperation wird möglicherweise nicht ausgeführt" angezeigt, wenn sich Ihre Codierung in Multithread-Szenarien befindet. Die unterste Ursache ist, dass Serialisierungsklassen die Sammlung über den Enumerator durchlaufen. Daher besteht das Problem darin, dass versucht wird, eine Sammlung zu durchlaufen, während sie geändert wird.
Erste Lösung: Wir können lock einfach als Synchronisationslösung verwenden, um sicherzustellen, dass die Operation für das List-Objekt jeweils nur von einem Thread ausgeführt werden kann. Offensichtlich erhalten Sie eine Leistungsbeeinträchtigung: Wenn Sie eine Sammlung dieses Objekts serialisieren möchten, wird für jedes Objekt die Sperre angewendet.
Nun, .NET 4.0, das den Umgang mit Multithreading-Szenarien praktisch macht. Bei diesem Problem mit dem Serialisierungssammlungsfeld habe ich festgestellt, dass wir nur von der ConcurrentQueue-Klasse (Check MSDN) profitieren können, die eine thread-sichere und FIFO-Sammlung ist und Code sperrfrei macht.
Wenn Sie diese Klasse verwenden, ersetzen Sie in ihrer Einfachheit die Elemente, die Sie für Ihren Code ändern müssen, durch den Sammlungstyp. Fügen Sie mit Enqueue ein Element am Ende von ConcurrentQueue hinzu und entfernen Sie diesen Sperrcode. Wenn für das Szenario, an dem Sie arbeiten, Erfassungssachen wie List erforderlich sind, benötigen Sie ein paar weitere Codes, um ConcurrentQueue an Ihre Felder anzupassen.
Übrigens hat ConcurrentQueue aufgrund des zugrunde liegenden Algorithmus, der kein atomares Löschen der Sammlung erlaubt, keine Clear-Methode. Sie müssen es also selbst tun. Der schnellste Weg besteht darin, eine neue leere ConcurrentQueue für einen Ersatz neu zu erstellen.
quelle
Ich persönlich bin kürzlich in unbekanntem Code darauf gestoßen, wo es sich in einem offenen Datenbankkontext mit Linqs .Remove (item) befand. Ich hatte verdammt viel Zeit damit, das Fehler-Debugging zu finden, weil die Elemente beim ersten Iterieren entfernt wurden. Wenn ich versuchte, einen Schritt zurückzutreten und erneut einen Schritt zurückzutreten, war die Sammlung leer, da ich mich im selben Kontext befand!
quelle