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);
}
}
}
fuente
Respuestas:
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
A
Si tengo razón, el problema desaparecerá
Llamar
subscribers.Values.ToList()
copia los valores desubscribers.Values
a una lista separada al comienzo deforeach
. Nada más tiene acceso a esta lista (¡ni siquiera tiene un nombre de variable!), Por lo que nada puede modificarlo dentro del bucle.fuente
subscribers.Values
se está modificando dentro delforeach
bucle. Llamarsubscribers.Values.ToList()
copia los valores desubscribers.Values
a una lista separada al comienzo deforeach
. Nada más tiene acceso a esta lista (¡ni siquiera tiene un nombre de variable!) , Por lo que nada puede modificarlo dentro del bucle.ToList
también podría arrojarse si la colección se modificó mientrasToList
se ejecuta.ToList
No es una operación atómica. Lo que es aún más divertido,ToList
básicamente hace lo suyoforeach
internamente para copiar elementos en una nueva instancia de lista, lo que significa que solucionó unforeach
problema al agregar unaforeach
iteración adicional (aunque más rápida) .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()
:fuente
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:
Luego lo cambias a:
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.
fuente
También puede bloquear su diccionario de suscriptores para evitar que se modifique cada vez que se enlaza:
fuente
MarkerFrequencies
diccionario dentro del bloqueo, lo que significa que la instancia original ya no está bloqueada. También intente usar un enfor
lugar de unforeach
, vea esto y esto . Avísame si eso lo resuelve.System.Collections.Concurrent
espacio de nombres.¿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
Aquí hay una publicación de blog sobre esta solución.
Y para una inmersión profunda en StackOverflow: ¿Por qué ocurre este error?
fuente
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.
fuente
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:
fuente
Tuve el mismo problema, y se resolvió cuando usé un
for
bucle en lugar deforeach
.fuente
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:
fuente
He visto muchas opciones para esto, pero para mí esta fue la mejor.
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:
fuente
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áslock
, 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í:
Esto puede ser especialmente útil si tiene un miembro público. Otras clases siempre pueden
foreach
usar los tipos inmutables sin preocuparse por la modificación de la colección.fuente
Aquí hay un escenario específico que garantiza un enfoque especializado:
Dictionary
se enumera con frecuencia.Dictionary
se modifica con poca frecuencia.En este escenario, crear una copia de
Dictionary
(o elDictionary.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 verIEnumerator
algunasDictionary
excepciones 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:Ejemplo de uso:
Desafortunadamente, esta idea no se puede usar actualmente con la clase
Dictionary
en .NET Core 3.0, porque esta clase no arroja una excepción de Colección modificada cuando se enumeraRemove
yClear
se 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>
ySortedDictionary<T,V>
. Solo los dos métodos de laDictionary
clase 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
EnumerableSnapshot
constructor '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.
fuente
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.
fuente
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.
fuente
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.
fuente
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!
fuente