La colección fue modificada; la operación de enumeración puede no ejecutarse

911

No puedo llegar al final de este error, porque cuando se adjunta el depurador, parece que no ocurre. Debajo está el código.

Este es un servidor WCF en un servicio de Windows. El servicio llama al método NotifySubscribers cada vez que hay un evento de datos (a intervalos aleatorios, pero no muy a menudo, aproximadamente 800 veces por día).

Cuando un cliente de Windows Forms se suscribe, la ID del suscriptor se agrega al diccionario de suscriptores, y cuando el cliente se da de baja, se elimina del diccionario. El error ocurre cuando (o después) un cliente se da de baja. Parece que la próxima vez que se llame al método NotifySubscribers (), el bucle foreach () falla con el error en la línea de asunto. El método escribe el error en el registro de la aplicación como se muestra en el código a continuación. Cuando se adjunta un depurador y un cliente se da de baja, el código se ejecuta bien.

¿Ves algún problema con este código? ¿Necesito hacer que el diccionario sea seguro para subprocesos?

[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);
        }
    }
}
cdonner
fuente
en mi caso fue un efecto colateral porque estaba usando unos pocos .Include ("tabla") que se modificaron durante el proceso, lo que no es muy obvio al leer el código. Sin embargo, tuve la suerte de que esas inclusiones no fueran necesarias (sí, código antiguo y sin mantenimiento) y resolví mi problema simplemente eliminándolas
Adi

Respuestas:

1632

Lo que probablemente esté sucediendo es que SignalData está cambiando indirectamente el diccionario de suscriptores bajo el capó durante el ciclo y lo está llevando a ese mensaje. Puede verificar esto cambiando

foreach(Subscriber s in subscribers.Values)

A

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

Si tengo razón, el problema desaparecerá

Llamar subscribers.Values.ToList()copia los valores de subscribers.Valuesa una lista separada al comienzo de foreach. Nada más tiene acceso a esta lista (¡ni siquiera tiene un nombre de variable!), Por lo que nada puede modificarlo dentro del bucle.

JaredPar
fuente
14
Por cierto .ToList () está presente en System.Core dll que no es compatible con las aplicaciones .NET 2.0. Por lo tanto, es posible que deba cambiar su aplicación de destino a .Net 3.5
mishal153
6060
No entiendo por qué hiciste una ToList y por qué eso soluciona todo
PositiveGuy
204204
@CoffeeAddict: El problema es que subscribers.Valuesse está modificando dentro del foreachbucle. Llamar subscribers.Values.ToList()copia los valores de subscribers.Valuesa una lista separada al comienzo de foreach. Nada más tiene acceso a esta lista (¡ni siquiera tiene un nombre de variable!) , Por lo que nada puede modificarlo dentro del bucle.
BlueRaja - Danny Pflughoeft
31
Tenga en cuenta que ToListtambién podría arrojarse si la colección se modificó mientras ToListse ejecuta.
Sriram Sakthivel
16
Estoy bastante seguro de que pensar no soluciona el problema, sino que simplemente hace que sea más difícil de reproducir. ToListNo es una operación atómica. Lo que es aún más divertido, ToListbásicamente hace lo suyo foreachinternamente para copiar elementos en una nueva instancia de lista, lo que significa que solucionó un foreachproblema al agregar una foreachiteración adicional (aunque más rápida) .
Groo
113

Cuando un suscriptor cancela su suscripción, está cambiando el contenido de la colección de suscriptores durante la enumeración.

Hay varias formas de solucionar esto, una de ellas es cambiar el bucle for para usar un explícito .ToList():

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

Una forma más eficiente, en mi opinión, es tener otra lista en la que declares que pones todo lo que se va a "eliminar". Luego, después de finalizar su ciclo principal (sin .ToList ()), realiza otro ciclo sobre la lista "para ser eliminado", eliminando cada entrada a medida que sucede. Entonces en tu clase agregas:

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

Luego lo cambias a:

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

Esto no solo resolverá su problema, sino que evitará que tenga que seguir creando una lista desde su diccionario, lo cual es costoso si hay muchos suscriptores allí. Suponiendo que la lista de suscriptores que se eliminará en cualquier iteración dada sea inferior al número total en la lista, esto debería ser más rápido. Pero, por supuesto, siéntase libre de perfilarlo para asegurarse de que ese sea el caso si hay alguna duda en su situación de uso específica.

x4000
fuente
99
Espero que valga la pena considerar esto si tiene un que está trabajando con colecciones más grandes. Si es pequeño, probablemente solo haga una Lista y siga adelante.
Karl Kieninger
42

También puede bloquear su diccionario de suscriptores para evitar que se modifique cada vez que se enlaza:

 lock (subscribers)
 {
         foreach (var subscriber in subscribers)
         {
               //do something
         }
 }
Mohammad Sepahvand
fuente
2
¿Es ese un ejemplo completo? Tengo una clase (_dictionary obj a continuación) que contiene un Diccionario genérico <string, int> llamado MarkerFrequencies, pero hacer esto no resolvió instantáneamente el bloqueo: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair <string, int> pair in _dictionary.MarkerFrequencies) {...}}
Jon Coombs
44
@JCoombs es posible que esté modificando, posiblemente reasignando el MarkerFrequenciesdiccionario dentro del bloqueo, lo que significa que la instancia original ya no está bloqueada. También intente usar un en forlugar de un foreach, vea esto y esto . Avísame si eso lo resuelve.
Mohammad Sepahvand
1
Bueno, finalmente pude solucionar este error, y estos consejos fueron útiles, ¡gracias! Mi conjunto de datos era pequeño y esta operación era solo una actualización de la pantalla de la interfaz de usuario, por lo que iterar sobre una copia parecía lo mejor. (También experimenté con el uso de for en lugar de foreach después de bloquear las frecuencias Marker en ambos subprocesos. Esto evitó el bloqueo, pero parecía requerir más trabajo de depuración. E introdujo más complejidad. Mi única razón para tener dos subprocesos aquí es permitir que el usuario cancele una operación, por cierto. La interfaz de usuario no modifica directamente esos datos.)
Jon Coombs
99
El problema es que, para aplicaciones grandes, los bloqueos pueden ser un éxito importante en el rendimiento; es mejor usar una colección en el System.Collections.Concurrentespacio de nombres.
BrainSlugs83
28

¿Por qué este error?

En general, las colecciones .Net no admiten ser enumeradas y modificadas al mismo tiempo. Si intenta modificar la lista de recopilación durante la enumeración, genera una excepción. Entonces, el problema detrás de este error es que no podemos modificar la lista / diccionario mientras estamos recorriendo el mismo.

Una de las soluciones

Si iteramos un diccionario usando una lista de sus claves, en paralelo podemos modificar el objeto del diccionario, ya que estamos iterando a través de la colección de claves y no del diccionario (e iterando su colección de claves).

Ejemplo

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

Aquí hay una publicación de blog sobre esta solución.

Y para una inmersión profunda en StackOverflow: ¿Por qué ocurre este error?

abierto y libre
fuente
¡Gracias! Una explicación clara de por qué sucede e inmediatamente me dio la forma de resolver esto en mi aplicación.
Kim Crosser
5

En realidad, el problema me parece que está eliminando elementos de la lista y espera continuar leyendo la lista como si nada hubiera pasado.

Lo que realmente necesita hacer es comenzar desde el final y volver al principio. Incluso si elimina elementos de la lista, podrá continuar leyéndolo.

luc.rg.roy
fuente
No veo cómo eso haría alguna diferencia. Si elimina un elemento en el medio de la lista, aún arrojaría un error, ya que no se puede encontrar el elemento al que intenta acceder.
Zapnologica
44
@Zapnologica la diferencia es que no enumeraría la lista, en lugar de hacer un for / each, estaría haciendo un for / next y accediendo a él por un entero, definitivamente puede modificar una lista a en un for / next loop, pero nunca en un for / each loop (porque for / each enumerates ): también puede hacerlo hacia adelante en un for / next, siempre que tenga una lógica adicional para ajustar sus contadores, etc.
BrainSlugs83
4

InvalidOperationException : se ha producido una InvalidOperationException. Informa una "colección modificada" en un bucle foreach

Utilice la declaración de interrupción, una vez que se elimina el objeto.

ex:

ArrayList list = new ArrayList(); 

foreach (var item in list)
{
    if(condition)
    {
        list.remove(item);
        break;
    }
}
vivek
fuente
Solución buena y simple si sabe que su lista tiene como máximo un elemento que debe eliminarse.
Tawab Wakil
3

Tuve el mismo problema, y ​​se resolvió cuando usé un forbucle en lugar de 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);
}
Daniel Moreshet
fuente
11
Este código es incorrecto y omitirá algunos elementos de la colección si se elimina algún elemento. Por ejemplo: tiene var arr = ["a", "b", "c"] y en la primera iteración (i = 0) elimina el elemento en la posición 0 (elemento "a"). Después de esto, todos los elementos de la matriz se moverán una posición hacia arriba y la matriz será ["b", "c"]. Entonces, en la próxima iteración (i = 1) verificará el elemento en la posición 1 que será "c" no "b". Esto está mal. Para arreglar eso, tienes que moverte de abajo hacia arriba
Kaspars Ozols
3

Bien, entonces lo que me ayudó fue iterar hacia atrás. Estaba tratando de eliminar una entrada de una lista pero iterando hacia arriba y arruinó el ciclo porque la entrada ya no existía:

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

                            myList.RemoveAt(x);

                        }
Mark Aven
fuente
2

He visto muchas opciones para esto, pero para mí esta fue la mejor.

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

Luego simplemente recorra la colección.

Tenga en cuenta que una ListItemCollection puede contener duplicados. Por defecto, no hay nada que impida que se agreguen duplicados a la colección. Para evitar duplicados, puede hacer esto:

ListItemCollection collection = new ListItemCollection();
            foreach (ListItem item in ListBox1.Items)
            {
                if (item.Selected && !collection.Contains(item))
                    collection.Add(item);
            }
Miguel
fuente
¿Cómo podría este código evitar que se ingresen duplicados en la base de datos? He escrito algo similar y cuando agrego nuevos usuarios desde el cuadro de lista, si accidentalmente mantengo uno seleccionado que ya está en la lista, creará una entrada duplicada. ¿Tienes alguna sugerencia @Mike?
Jamie
1

La respuesta aceptada es imprecisa e incorrecta en el peor de los casos. Si se realizan cambios duranteToList() , aún puede terminar con un error. Además lock, qué rendimiento y seguridad de hilos deben tenerse en cuenta si tiene un miembro público, una solución adecuada puede ser usar tipos inmutables .

En general, un tipo inmutable significa que no puede cambiar el estado una vez creado. Entonces su código debería verse así:

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

Esto puede ser especialmente útil si tiene un miembro público. Otras clases siempre pueden foreachusar los tipos inmutables sin preocuparse por la modificación de la colección.

joe
fuente
1

Aquí hay un escenario específico que garantiza un enfoque especializado:

  1. El Dictionaryse enumera con frecuencia.
  2. El Dictionaryse modifica con poca frecuencia.

En este escenario, crear una copia de Dictionary(o el Dictionary.Values) antes de cada enumeración puede ser bastante costoso. Mi idea sobre la solución de este problema es reutilizar la misma copia en caché en varias enumeraciones, y ver IEnumeratoralgunas Dictionaryexcepciones originales . El enumerador se almacenará en caché junto con los datos copiados y se interrogará antes de comenzar una nueva enumeración. En caso de una excepción, la copia en caché se descartará y se creará una nueva. Aquí está mi implementación de esta 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);
}

Ejemplo de uso:

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

Desafortunadamente, esta idea no se puede usar actualmente con la clase Dictionaryen .NET Core 3.0, porque esta clase no arroja una excepción de Colección modificada cuando se enumera Removey Clearse invocan los métodos . Todos los demás contenedores que verifiqué se comportan de manera consistente. He comprobado sistemáticamente estas clases: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V>y SortedDictionary<T,V>. Solo los dos métodos de la Dictionaryclase mencionados anteriormente en .NET Core no invalidan la enumeración.


Actualización: solucioné el problema anterior al comparar también las longitudes de la colección en caché y la original. Esta solución supone que el diccionario se pasará directamente como un argumento para el EnumerableSnapshotconstructor 's, y su identidad no será ocultado por (por ejemplo) una proyección como: dictionary.Select(e => e).ΤοEnumerableSnapshot().


Importante: la clase anterior no es segura para subprocesos. Está destinado a ser utilizado desde el código que se ejecuta exclusivamente en un solo hilo.

Theodor Zoulias
fuente
0

Puede copiar el objeto de diccionario de los suscriptores a un mismo tipo de objeto de diccionario temporal y luego iterar el objeto de diccionario temporal usando el bucle foreach.

Rezoan
fuente
(Esta publicación no parece proporcionar una respuesta de calidad a la pregunta. Edite su respuesta o simplemente publíquela como un comentario a la pregunta).
sɐunıɔ ןɐ qɐp
0

Entonces, una forma diferente de resolver este problema sería en lugar de eliminar los elementos, crear un nuevo diccionario y solo agregar los elementos que no desea eliminar y luego reemplazar el diccionario original con el nuevo. No creo que esto sea demasiado problema de eficiencia porque no aumenta el número de veces que iteras sobre la estructura.

prefecto vado
fuente
0

Hay un enlace donde se elaboró ​​muy bien y también se ofrece una solución. Pruébelo si tiene la solución adecuada, publique aquí para que otros puedan entenderlo. La solución dada está bien, entonces al igual que la publicación para que otros puedan probar esta solución.

para su referencia enlace original: - https://bensonxion.wordpress.com/2012/05/07/serializing-an-ienumerable-produces-collection-was-modified-enumeration-operation-may-not-execute/

Cuando usamos las clases de serialización .Net para serializar un objeto donde su definición contiene un tipo Enumerable, es decir, colección, obtendrá fácilmente InvalidOperationException diciendo "La colección se modificó; la operación de enumeración puede no ejecutarse" donde su codificación se encuentra en escenarios de subprocesos múltiples. La causa inferior es que las clases de serialización iterarán a través de la colección a través del enumerador, como tal, el problema va a intentar iterar a través de una colección mientras se modifica.

Primera solución, simplemente podemos usar el bloqueo como solución de sincronización para garantizar que la operación del objeto Lista solo se pueda ejecutar desde un hilo a la vez. Obviamente, obtendrá una penalización de rendimiento que si desea serializar una colección de ese objeto, entonces, para cada uno de ellos, se aplicará el bloqueo.

Bueno, .Net 4.0, que hace que lidiar con escenarios de subprocesos múltiples sea útil. para este problema de serialización del campo Colección, descubrí que podemos aprovechar la clase ConcurrentQueue (Verificar MSDN), que es una colección FIFO segura para subprocesos y hace que el código esté libre de bloqueos.

Usando esta clase, en su simplicidad, las cosas que necesita modificar para su código están reemplazando el tipo de Colección con él, use Enqueue para agregar un elemento al final de ConcurrentQueue, elimine esos códigos de bloqueo. O, si el escenario en el que está trabajando requiere elementos de colección como List, necesitará algunos códigos más para adaptar ConcurrentQueue a sus campos.

Por cierto, ConcurrentQueue no tiene un método Clear debido al algoritmo subyacente que no permite borrar atómicamente la colección. por lo que debe hacerlo usted mismo, la forma más rápida es volver a crear un nuevo ConcurrentQueue vacío para su reemplazo.

usuario8851697
fuente
-1

Personalmente, me encontré con esto recientemente en un código desconocido donde estaba en un dbcontext abierto con .Quitar (elemento) de Linq. Me costó mucho localizar el error de depuración porque los elementos se eliminaron la primera vez que iteraron, y si intenté dar un paso atrás y volver a pasar, la colección estaba vacía, ¡ya que estaba en el mismo contexto!

Michael Sefranek
fuente