Se modificó la colección; la operación de enumeración puede no ejecutarse

No puedo llegar al final de este error, porque cuando se adjunta el depurador, no parece ocurrir.

Se modificó la colección; la operación de enumeración puede no ejecutarse

A continuación se muestra el código.

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

Cuando un cliente de Windows Forms se suscribe, el identificador de suscriptor se agrega al diccionario de suscriptores y, cuando el cliente cancela la suscripción, se elimina del diccionario. El error ocurre cuando (o después) un cliente se da de baja. Parece que la próxima vez que el NotifySubscribers() se llama al método, el foreach() El bucle 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 siguiente código. Cuando se adjunta un depurador y un cliente se da de baja, el código se ejecuta correctamente.

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

preguntado el 03 de marzo de 09 a las 00:03

en mi caso, fue un efecto colateral porque estaba usando algunos .Include ("tabla") que se modificaron durante el proceso, no muy obvio al leer el código. sin embargo, tuve suerte de que esas inclusiones no fueran necesarias (¡sí! código antiguo, sin mantener) y resolví mi problema simplemente eliminándolos -

Por favor, eche un vistazo a la respuesta que proporcionó @joe. Es una solución mucho mejor en muchos casos. stackoverflow.com/a/57799537/10307728 -

17 Respuestas

Lo que probablemente suceda es que SignalData está cambiando indirectamente el diccionario de los suscriptores bajo el capó durante el ciclo y conduce 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.Values a una lista separada al comienzo de la foreach. Nada más tiene acceso a esta lista (¡ni siquiera tiene un nombre de variable!), Por lo que nada puede modificarlo dentro del ciclo.

Respondido el 29 de junio de 20 a las 23:06

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

No entiendo por qué hiciste un ToList y por qué eso soluciona todo. ChicoPositivo

@CoffeeAddict: El problema es que subscribers.Values está siendo modificado dentro del foreach círculo. Vocación subscribers.Values.ToList() copia los valores de subscribers.Values a una lista separada al comienzo de la 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

Tenga en cuenta que ToList también podría lanzar si la colección se modificó mientras ToList está ejecutando. - Sriram Sakthivel

Estoy bastante seguro de que pensar no soluciona el problema, sino que simplemente hace que sea más difícil de reproducir. ToList no es una operación atómica. ¿Qué es aún más divertido? ToList básicamente hace lo suyo foreach internamente para copiar elementos en una nueva instancia de lista, lo que significa que corrigió un foreach problema agregando un adicional (aunque más rápido) foreach iteración. - Groo

Cuando un suscriptor se da de baja, está cambiando el contenido de la colección de suscriptores durante la enumeración.

Hay varias formas de solucionar este problema, una de las cuales es cambiar el bucle for para usar un código explícito. .ToList():

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

Respondido 11 Oct 17, 09:10

Una forma más eficiente, en mi opinión, es tener otra lista en la que declaras que pones todo lo que "se eliminará". Luego, después de terminar su ciclo principal (sin el .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 cambia 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án en cualquier iteración dada es menor que el número total de la lista, esto debería ser más rápido. Pero, por supuesto, no dude en perfilarlo para asegurarse de que ese sea el caso si hay alguna duda en su situación de uso específica.

Respondido 15 Jul 14, 12:07

Espero que valga la pena considerar esto si tiene una colección más grande. Si es pequeño, probablemente solo ToList y seguir adelante. - Karl Kieninger

También puede bloquear el diccionario de sus suscriptores para evitar que se modifique cada vez que se repita:

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

contestado el 23 de mayo de 12 a las 20:05

¿Es ese un ejemplo completo? Tengo una clase (_dictionary obj a continuación) que contiene un diccionario genérico llamado MarkerFrequencies, pero hacer esto no resolvió instantáneamente el bloqueo: lock (_dictionary.MarkerFrequencies) {foreach (KeyValuePair par en _dictionary.MarkerFrequencies) {...}} - jon coombs

@JCoombs es posible que esté modificando, posiblemente reasignando el MarkerFrequencies diccionario dentro del bloqueo, lo que significa que la instancia original ya no está bloqueada. También intente usar un for en lugar de un foreach, Ver así y así. Avísame si eso lo resuelve. - Mohammad Sepahvand

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 lo mejor parecía iterar sobre una copia. (También experimenté con el uso de for en lugar de foreach después de bloquear MarkerFrequencies 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

El problema es que, para aplicaciones grandes, las cerraduras pueden ser un gran impacto en el rendimiento; es mejor usar una colección en System.Collections.Concurrent espacio de nombres. - cerebroslugs83

¿Por qué este error?

En general, las colecciones .Net no admiten que se enumeren y modifiquen al mismo tiempo. Si intenta modificar la lista de colecciones 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 recorremos el mismo.

Una de las soluciones

Si iteramos un diccionario usando una lista de sus claves, en paralelo podemos modificar el objeto 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 blog sobre esta solución.

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

Respondido 22 Feb 19, 06:02

¡Gracias! Una explicación clara de por qué sucede e inmediatamente me dio la forma de resolver esto en mi aplicación. - kim crosser

A partir de .NET Core 3.0 con C # 8.0, un el diccionario se puede modificar durante la enumeración (foreach) a través de .Remove y .Clear solamente. Esto no se aplica a otras colecciones. - Súper Jade

Bien, lo que me ayudó fue iterar al revé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);
}

Respondido el 05 de Septiembre de 20 a las 00:09

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 necesitas hacer es empezar desde el final y volver al principio. Incluso si elimina elementos de la lista, podrá seguir leyendo.

contestado el 23 de mayo de 12 a las 17:05

No veo cómo eso haría alguna diferencia. Si elimina un elemento en el medio de la lista, todavía arrojaría un error ya que el elemento al que intenta acceder no se puede encontrar. - Zapnológica

@Zapnologica, la diferencia es que tú no estarías enumerando la lista: en lugar de hacer un for / each, estaría haciendo un for / next y accediendo a él por entero; definitivamente puede modificar una lista a en un bucle for / next, pero nunca en un bucle for / each (porque para / cada uno enumera) - también puede hacerlo avanzando en un for / next, siempre que tenga lógica adicional para ajustar sus contadores, etc. - cerebroslugs83

Excepción de operación no válida- Se ha producido una InvalidOperationException. Informa que una "colección fue modificada" en un bucle foreach

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

ex:

ArrayList list = new ArrayList(); 

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

Respondido 17 ago 17, 15:08

Solución buena y sencilla si sabe que su lista tiene como máximo un elemento que debe eliminarse. - Tawab Wakil

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

En general, un tipo inmutable significa que no puede cambiar su 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 resultar especialmente útil si tiene un miembro público. Otras clases siempre pueden foreach en los tipos inmutables sin preocuparse de que se modifique la colección.

Respondido el 05 de Septiembre de 19 a las 07:09

Tuve el mismo problema y se resolvió cuando usé un for bucle 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);
}

Respondido 07 Feb 18, 12:02

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 solucionarlo, debes moverte de abajo hacia arriba - Kaspars Özoles

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 recorre la colección.

Tenga en cuenta que ListItemCollection puede contener duplicados. De forma predeterminada, nada impide 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);
            }

respondido 16 mar '18, 13:03

¿Cómo evitaría este código 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, se creará una entrada duplicada. ¿Tienes alguna sugerencia @ Mike? - Jamie

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

  1. LA Dictionary se enumera con frecuencia.
  2. LA Dictionary se modifica con poca frecuencia.

En este escenario, crear una copia del Dictionary (O la Dictionary.Values) antes de cada enumeración puede resultar bastante costoso. Mi idea para resolver este problema es reutilizar la misma copia en caché en múltiples enumeraciones y ver una IEnumerator del original Dictionary para excepciones. 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. Dictionary en .NET Core 3.0, porque esta clase no lanza un La colección fue modificada excepción cuando se enumeran y los métodos Remove y Clear se invocan. Todos los demás contenedores que verifiqué se comportan de manera consistente. Verifiqué sistemáticamente estas clases: List<T>, Collection<T>, ObservableCollection<T>, HashSet<T>, SortedSet<T>, Dictionary<T,V> y SortedDictionary<T,V>. Sólo los dos métodos antes mencionados de la Dictionary class en .NET Core no invalidan la enumeración.


Actualizar: Solucioné el problema anterior comparando también las longitudes de la colección en caché y la original. Esta corrección asume que el diccionario se pasará directamente como un argumento a la EnumerableSnapshot's constructor, y su identidad no estará oculta por (por ejemplo) una proyección como: dictionary.Select(e => e).ΤοEnumerableSnapshot().


Importante: La clase anterior es no a salvo de amenazas. Está destinado a ser utilizado a partir de código que se ejecuta exclusivamente en un solo hilo.

contestado el 17 de mayo de 20 a las 18:05

Quiero señalar otro caso que no se refleja en ninguna de las respuestas. tengo un Dictionary<Tkey,TValue> compartido en una aplicación de subprocesos múltiples, que utiliza un ReaderWriterLockSlim para proteger las operaciones de lectura y escritura. Este es un método de lectura que lanza la excepción:

public IEnumerable<Data> GetInfo()
{
    List<Data> info = null;
    _cacheLock.EnterReadLock();
    try
    {
        info = _cache.Values.SelectMany(ce => ce.Data); // Ad .Tolist() to avoid exc.
    }
    finally
    {
        _cacheLock.ExitReadLock();
    }
    return info;
}

En general, funciona bien, pero de vez en cuando obtengo la excepción. El problema es una sutileza de LINQ: este código devuelve un IEnumerable<Info>, que aún no se enumera después de salir de la sección protegida por el candado. Por lo tanto, otros subprocesos pueden cambiarlo antes de enumerarlo, lo que lleva a la excepción. La solución es forzar la enumeración, por ejemplo con .ToList() como se muestra en el comentario. De esta forma, el enumerable ya está enumerado antes de salir de la sección protegida.

Por lo tanto, si usa LINQ en una aplicación multiproceso, tenga en cuenta que debe materializar siempre las consultas antes de salir de las regiones protegidas.

Respondido el 27 de enero de 21 a las 13:01

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.

contestado el 29 de mayo de 13 a las 14:05

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

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 un problema de eficiencia demasiado grande porque no aumenta la cantidad de veces que se itera sobre la estructura.

Respondido 04 Oct 13, 15:10

Hay un enlace donde se elaboró ​​muy bien y también se da una solución. Pruébelo si tiene la solución adecuada, publique aquí para que otros puedan entender. Dada la solución está bien, entonces me gusta 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 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 que dice "La colección fue modificada; la operación de enumeración puede no ejecutarse" donde su codificación está en escenarios de múltiples subprocesos. La causa final 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 radica en intentar iterar a través de una colección mientras la modifica.

Primera solución, podemos simplemente usar el bloqueo como una solución de sincronización para asegurarnos de que la operación al objeto List 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, se aplicará el bloqueo para cada uno de ellos.

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 (Check MSDN), que es una colección FIFO segura para subprocesos y hace que el código no se bloquee.

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 recopilación como List, necesitará un poco más de código para adaptar ConcurrentQueue a sus campos.

Por cierto, ConcurrentQueue no tiene un método Clear debido al algoritmo subyacente que no permite la limpieza atómica de la colección. por lo que debe hacerlo usted mismo, la forma más rápida es volver a crear una nueva ConcurrentQueue vacía para reemplazarla.

Respondido el 13 de diciembre de 18 a las 05:12

Esta forma debería cubrir una situación de simultaneidad cuando la función se vuelve a llamar mientras aún se está ejecutando (y los elementos deben usarse solo una vez):

 while (list.Count > 0)
 {
    string Item = list[0];
    list.RemoveAt(0);
 
    // do here what you need to do with item
 
 } 
 

Si se llama a la función mientras aún se está ejecutando, los elementos no se repetirán desde el primero de nuevo, ya que se eliminarán tan pronto como se usen. No debería afectar mucho al rendimiento para listas pequeñas.

respondido 12 nov., 20:11

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.