Se modificó la colección; Es posible que la operación de enumeración no se ejecute

Resuelto cdonner asked hace 15 años • 16 respuestas

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

Se modificó la colección; Es posible que la operación de enumeración no se ejecute

A continuación se muestra el código.

Este es un servidor WCF en un servicio de Windows. El servicio NotifySubscribers()llama al método 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, el ID del 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 NotifySubscribers()que se llama al método, el foreach()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 código siguiente. 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 avatar Mar 03 '09 09:03 cdonner
Aceptado

Lo que probablemente sucede es que SignalDataindirectamente cambia el diccionario de suscriptores bajo el capó durante el ciclo y conduce a ese mensaje. Puedes verificar esto cambiando

foreach(Subscriber s in subscribers.Values)

A

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

Si estoy en lo cierto, el problema desaparecerá.

La llamada subscribers.Values.ToList()copia los valores de subscribers.Valuesen una lista separada al comienzo del archivo foreach. Nada más tiene acceso a esta lista (¡ni siquiera tiene un nombre de variable!), por lo que nada puede modificarla dentro del bucle.

JaredPar avatar Mar 03 '2009 02:03 JaredPar

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

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

public void NotifySubscribers(DataRecord sr)  
{
    foreach(Subscriber s in subscribers.Values.ToList())
    {
                                              ^^^^^^^^^  
        ...
Mitch Wheat avatar Mar 03 '2009 02:03 Mitch Wheat

En mi opinión, una forma más eficiente es tener otra lista en la que declare que coloca todo lo que "debe eliminarse". Luego, después de terminar el bucle principal (sin .ToList()), realiza otro bucle sobre la lista "para eliminar", 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 sólo resolverá su problema, sino que también evitará que tenga que seguir creando una lista a partir de 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 determinada sea menor que el número total de la lista, esto debería ser más rápido. Pero, por supuesto, siéntase libre de crear un perfil para asegurarse de que ese sea el caso si tiene alguna duda sobre su situación de uso específica.

Chris McElligott Park avatar Mar 03 '2009 06:03 Chris McElligott Park

¿Por qué este error?

En general, las colecciones .Net no admiten la enumeración y modificación al mismo tiempo. Si intenta modificar la lista de recopilación durante la enumeración, se 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 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 profundizar en StackOverflow: ¿Por qué ocurre este error?

Language Lassi avatar Nov 11 '2014 12:11 Language Lassi