c# - Sammlung wurde geändert;Aufzählungsoperation wird möglicherweise nicht ausgeführt




wcf concurrency dictionary thread-safety (10)

InvalidOperationException - Eine InvalidOperationException ist aufgetreten. Es meldet eine "Sammlung wurde modifiziert" 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;
    }
}

Ich kann dem Fehler nicht auf den Grund gehen, denn wenn der Debugger angehängt ist, scheint es nicht zu kommen. Unten ist der Code.

Dies ist ein WCF-Server in einem Windows-Dienst. Die Methode NotifySubscribers wird vom Dienst aufgerufen, wenn ein Datenereignis vorliegt (in zufälligen Intervallen, aber nicht sehr oft - etwa 800 Mal pro Tag).

Wenn ein Windows Forms-Client subskribiert wird, wird die Abonnenten-ID dem Wörterbuch des Abonnenten hinzugefügt, und wenn der Client sich abmeldet, wird er aus dem Wörterbuch gelöscht. Der Fehler tritt auf, wenn (oder nachdem) ein Client sich 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 dargestellt in das Anwendungsprotokoll. Wenn ein Debugger angeschlossen ist und ein Client sich abmeldet, wird der Code ordnungsgemäß ausgeführt.

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

Ich habe viele Möglichkeiten dafür gesehen, aber für mich war dieser der Beste.

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

Dann einfach die Sammlung durchlaufen.

Beachten Sie, dass eine ListItemCollection Duplikate enthalten kann. Standardmäßig verhindert nichts, 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);
            }

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

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


Ein effizienterer Weg ist meiner Meinung nach, eine andere Liste zu haben, in der Sie erklären, dass Sie alles, was "entfernt" werden soll, hineinlegen. Nachdem Sie Ihre Hauptschleife beendet haben (ohne die .ToList ()), führen Sie eine weitere Schleife über die Liste "Entfernt zu werden" aus, indem Sie jeden Eintrag entfernen, während er passiert. Also fügst du in deiner Klasse hinzu:

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

Dann änderst du es zu:

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, es wird Sie daran hindern, weiterhin eine Liste aus Ihrem Wörterbuch zu erstellen, was teuer ist, wenn viele Abonnenten dort sind. Unter der Annahme, dass die Liste der Teilnehmer, die bei einer gegebenen Iteration entfernt werden, niedriger ist als die Gesamtanzahl in der Liste, sollte dies schneller sein. Aber natürlich können Sie sich auch ein Profil erstellen, um sicher zu sein, dass dies der Fall ist, wenn Zweifel an Ihrer spezifischen Nutzungssituation bestehen.


So eine andere Möglichkeit, dieses Problem zu lösen wäre, anstatt die Elemente zu entfernen, erstellen Sie ein neues Wörterbuch und fügen Sie nur die Elemente hinzu, die Sie nicht entfernen wollten, und ersetzen Sie dann das ursprüngliche Wörterbuch durch das neue. Ich glaube nicht, dass dies ein zu großes Effizienzproblem ist, weil es die Anzahl der Wiederholungen der Struktur nicht erhöht.


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

foreach(Subscriber s in subscribers.Values)

Zu

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

Wenn ich richtig liege, wird das Problem verschwinden


Wenn ein Abonnent sich abmeldet, ändern Sie den Inhalt der Auflistung der Abonnenten während der Enumeration.

Es gibt mehrere Möglichkeiten, dies zu beheben, indem man die for-Schleife ändert, um eine explizite .ToList() :

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

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

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

Sie können das Dictionary-Objekt des Abonnenten in den gleichen Typ des temporären Dictionary-Objekts kopieren und anschließend das temporäre Dictionary-Objekt mithilfe der foreach-Schleife iterieren.


Verwenden Sie anstelle von foreach () eine for () -Schleife mit einem numerischen Index.





c# wcf concurrency dictionary thread-safety