valida - uso de invoke en c#




Eventos de C#y seguridad de subprocesos (10)

ACTUALIZAR

A partir de C # 6, la respuesta a esta pregunta es:

SomeEvent?.Invoke(this, e);

Frecuentemente escucho / leo los siguientes consejos:

Siempre haga una copia de un evento antes de verificarlo para ver si está null y dispararlo. Esto eliminará un problema potencial con el subprocesamiento en el que el evento se vuelve null en la ubicación correcta entre el lugar donde se verificó el nulo y el lugar donde se activó el evento:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Actualizado : pensé en leer acerca de las optimizaciones que esto también podría requerir que el miembro del evento sea volátil, pero Jon Skeet afirma en su respuesta que el CLR no optimiza la copia.

Pero mientras tanto, para que este problema ocurra, otro hilo debe haber hecho algo como esto:

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...

La secuencia real podría ser esta mezcla:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;    
// Good, now we can be certain that OnTheEvent will not run...

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

El punto es que OnTheEvent ejecuta después de que el autor se haya dado de baja y, sin embargo, solo se ha dado de baja específicamente para evitar que eso suceda. Seguramente lo que realmente se necesita es una implementación de eventos personalizada con la sincronización adecuada en los add y remove . Y además, existe el problema de posibles interbloqueos si se mantiene un bloqueo mientras se dispara un evento.

Entonces, ¿esta es la Programación del Culto de Carga ? Parece que de esta manera: mucha gente debe estar dando este paso para proteger su código de múltiples subprocesos, cuando en realidad me parece que los eventos requieren mucho más cuidado que esto antes de que puedan usarse como parte de un diseño de subprocesos múltiples. . En consecuencia, las personas que no están tomando ese cuidado adicional también pueden ignorar este consejo: simplemente no es un problema para los programas de un solo hilo, y de hecho, dada la ausencia de volatile en la mayoría del código de ejemplo en línea, el consejo puede estar teniendo ningún efecto en absoluto.

(¿Y no es mucho más simple simplemente asignar el delegate { } vacío delegate { } en la declaración de miembro para que nunca tenga que verificar el null en primer lugar?)

Actualizado: en caso de que no estuviera claro, entendí la intención del consejo: evitar una excepción de referencia nula en todas las circunstancias. Mi punto es que esta excepción de referencia nula en particular solo puede ocurrir si otro subproceso se está eliminando del evento, y la única razón para hacerlo es garantizar que no se reciban más llamadas a través de ese evento, lo que claramente NO se logra con esta técnica . Estarías ocultando una condición de carrera, ¡sería mejor revelarla! Esa excepción nula ayuda a detectar un abuso de su componente. Si desea que su componente esté protegido contra el abuso, podría seguir el ejemplo de WPF: almacenar el ID de hilo en su constructor y luego lanzar una excepción si otro hilo intenta interactuar directamente con su componente. O bien, implemente un componente verdaderamente seguro para subprocesos (no es una tarea fácil).

Por lo tanto, sostengo que simplemente hacer este lenguaje de copia / cheque es la programación de cultos de carga, agregando desorden y ruido a su código. Proteger realmente contra otros hilos requiere mucho más trabajo.

Actualización en respuesta a las publicaciones del blog de Eric Lippert:

Así que hay una cosa importante que me había perdido acerca de los manejadores de eventos: "los manejadores de eventos deben ser robustos al ser convocados incluso después de que el evento haya sido cancelado", y obviamente, por lo tanto, solo debemos preocuparnos por la posibilidad del evento delegado siendo null . ¿Está ese requisito en los manejadores de eventos documentado en alguna parte?

Y así: "Hay otras formas de resolver este problema; por ejemplo, inicializar el controlador para tener una acción vacía que nunca se elimina. Pero hacer una comprobación nula es el patrón estándar".

Entonces, el único fragmento que queda de mi pregunta es, ¿por qué está explícito-nulo-verifique el "patrón estándar"? La alternativa, asignar el delegado vacío, requiere que solo se agregue = delegate {} a la declaración del evento, y esto elimina esas pequeñas pilas de ceremonia apestosa de cada lugar donde se levanta el evento. Sería fácil asegurarse de que el delegado vacío sea barato de instanciar. ¿O todavía me falta algo?

Seguramente debe ser que (como sugirió Jon Skeet) este es un consejo .NET 1.x que no se ha extinguido, como debería haber hecho en 2005.


"¿Por qué es explícito-nulo-compruebe el 'patrón estándar'?"

Sospecho que la razón de esto podría ser que la comprobación nula es más eficaz.

Si siempre suscribe un delegado vacío a sus eventos cuando se crean, habrá algunos gastos generales:

  • Costo de la construcción del delegado vacío.
  • Costo de construir una cadena de delegado para contenerla.
  • Costo de invocar al delegado sin sentido cada vez que se levanta el evento.

(Tenga en cuenta que los controles de UI a menudo tienen una gran cantidad de eventos, a los cuales la mayoría de los cuales nunca se ha suscrito. Tener que crear un suscriptor ficticio para cada evento y luego invocarlo probablemente sería un éxito significativo en el rendimiento).

Hice algunas pruebas de rendimiento para ver el impacto del enfoque de suscripción-delegado-vacío, y aquí están mis resultados:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

Tenga en cuenta que para el caso de cero o un suscriptor (común para los controles de IU, donde abundan los eventos), el evento preinicializado con un delegado vacío es notablemente más lento (más de 50 millones de iteraciones ...)

Para obtener más información y el código fuente, visite esta publicación de blog sobre seguridad de subprocesos de invocación de eventos .NET que publiqué justo el día antes de que se hiciera esta pregunta (!)

(Mi configuración de prueba puede ser defectuosa, por lo que puede descargar el código fuente e inspeccionarlo usted mismo. Cualquier comentario es muy apreciado.)


Así que llego un poco tarde a la fiesta aquí. :)

En cuanto al uso de null en lugar del patrón de objeto nulo para representar eventos sin suscriptores, considere este escenario. Debe invocar un evento, pero la construcción del objeto (EventArgs) no es trivial, y en el caso común su evento no tiene suscriptores. Sería beneficioso para usted si pudiera optimizar su código para verificar si tenía algún suscriptor antes de comprometer el esfuerzo de procesamiento para construir los argumentos e invocar el evento.

Con esto en mente, una solución es decir "bueno, cero suscriptores está representado por nulo". Luego simplemente realice la comprobación nula antes de realizar su operación costosa. Supongo que otra forma de hacer esto sería tener una propiedad Count en el tipo Delegado, por lo que solo realizaría la operación costosa si myDelegate.Count> 0. El uso de una propiedad Count es un patrón agradable que resuelve el problema original de permitir la optimización, y también tiene la agradable propiedad de poder invocarse sin causar una NullReferenceException.

Sin embargo, tenga en cuenta que, dado que los delegados son tipos de referencia, se les permite ser nulos. Tal vez simplemente no había una buena forma de ocultar este hecho bajo las coberturas y de admitir solo el patrón de objetos nulos para eventos, por lo que la alternativa podría haber forzado a los desarrolladores a verificar tanto los suscriptores nulos como los cero. Eso sería aún más feo que la situación actual.

Nota: Esto es pura especulación. No estoy involucrado con los lenguajes .NET o CLR.


Conecta todos tus eventos en la construcción y déjalos en paz. El diseño de la clase Delegado no puede manejar ningún otro uso correctamente, como explicaré en el párrafo final de esta publicación.

En primer lugar, no tiene sentido tratar de interceptar una notificación de evento cuando sus manejadores de eventos ya deben tomar una decisión sincronizada sobre si responder a la notificación o cómo hacerlo.

Cualquier cosa que pueda ser notificada, debe ser notificada. Si sus manejadores de eventos manejan las notificaciones de manera adecuada (es decir, tienen acceso a un estado de aplicación autorizada y responden solo cuando sea apropiado), entonces será correcto notificarles en cualquier momento y confiar en que responderán adecuadamente.

La única vez que no se debe notificar a un manejador que ocurrió un evento, es si el evento en realidad no ha ocurrido. Entonces, si no desea que se notifique a un controlador, deje de generar los eventos (es decir, deshabilite el control o lo que sea responsable de detectar y traer el evento a existencia en primer lugar).

Sinceramente, creo que la clase de delegado es insuperable. La fusión / transición a un MulticastDelegate fue un gran error, ya que efectivamente cambió la definición (útil) de un evento de algo que sucede en un solo instante en el tiempo, a algo que sucede durante un período de tiempo. Un cambio de este tipo requiere un mecanismo de sincronización que, lógicamente, pueda contraerlo nuevamente en un solo instante, pero el MulticastDelegate carece de tal mecanismo. La sincronización debe abarcar todo el intervalo de tiempo o el instante en que se produce el evento, de modo que una vez que una aplicación toma la decisión sincronizada de comenzar a manejar un evento, termina de manejarlo por completo (transaccionalmente). Con la caja negra que es la clase híbrida MulticastDelegate / Delegate, esto es casi imposible, así que adhiere a un solo suscriptor y / o implementa tu propio tipo de MulticastDelegate que tiene un controlador de sincronización que se puede quitar mientras la cadena del manejador es siendo utilizado / modificado . Recomiendo esto, porque la alternativa sería implementar la sincronización / integridad transaccional de forma redundante en todos sus controladores, lo que sería ridículamente / innecesariamente complejo.


El JIT no tiene permitido realizar la optimización de la que está hablando en la primera parte, debido a la condición. Sé que esto se planteó como un espectro hace un tiempo, pero no es válido. (Lo comprobé con Joe Duffy o Vance Morrison hace un tiempo; no puedo recordar cuál).

Sin el modificador volátil es posible que la copia local tomada esté desactualizada, pero eso es todo. No causará una NullReferenceException .

Y sí, ciertamente hay una condición de carrera, pero siempre la habrá. Supongamos que simplemente cambiamos el código a:

TheEvent(this, EventArgs.Empty);

Supongamos ahora que la lista de invocaciones para ese delegado tiene 1000 entradas. Es perfectamente posible que la acción al comienzo de la lista se haya ejecutado antes de que otro hilo cancele la suscripción de un controlador cerca del final de la lista. Sin embargo, ese controlador aún se ejecutará porque será una nueva lista. (Los delegados son inmutables). Por lo que puedo ver, esto es inevitable.

Usar un delegado vacío ciertamente evita el control de nulidad, pero no corrige la condición de carrera. Tampoco garantiza que siempre "vea" el último valor de la variable.


He estado usando este patrón de diseño para garantizar que los controladores de eventos no se ejecuten después de que se cancelen las suscripciones. Está funcionando bastante bien hasta ahora, aunque no he probado ningún perfil de rendimiento.

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

Actualmente trabajo principalmente con Mono para Android, y parece que a Android no le gusta cuando intentas actualizar una vista después de que su actividad se haya enviado a un segundo plano.


Para aplicaciones de un solo hilo, está correcto. Esto no es un problema.

Sin embargo, si está creando un componente que expone eventos, no hay garantía de que un consumidor de su componente no vaya a ser multihilo, en cuyo caso debe prepararse para lo peor.

El uso del delegado vacío resuelve el problema, pero también causa un impacto en el rendimiento en cada llamada al evento, y podría tener implicaciones de GC.

Tiene razón en que el consumidor debe darse de baja para que esto suceda, pero si superó la copia temporal, considere el mensaje que ya está en tránsito.

Si no usa la variable temporal y no usa el delegado vacío, y alguien se da de baja, obtiene una excepción de referencia nula, que es fatal, por lo que creo que el costo vale la pena.


Realmente nunca he considerado que esto sea un gran problema porque generalmente solo protejo contra este tipo de maldad potencial de subprocesos en métodos estáticos (etc.) en mis componentes reutilizables, y no hago eventos estáticos.

¿Lo estoy haciendo mal?


Según Jeffrey Richter en el libro CLR a través de C # , el método correcto es:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

Porque obliga a una copia de referencia. Para más información, vea su sección de Eventos en el libro.


Gracias por una discusión útil. Recientemente estuve trabajando en este problema e hice la siguiente clase, que es un poco más lenta, pero permite evitar llamadas a objetos desechados.

El punto principal aquí es que la lista de invocaciones puede modificarse incluso si se genera un evento.

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

Y el uso es:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

Prueba

Lo probé de la siguiente manera. Tengo un hilo que crea y destruye objetos como este:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

En un Bar(un objeto de escucha), el constructor me suscribo SomeEvent(que se implementa como se muestra arriba) y doy de baja en Dispose:

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

También tengo un par de hilos que plantean eventos en un bucle.

Todas estas acciones se realizan simultáneamente: muchos oyentes se crean y se destruyen y el evento se dispara al mismo tiempo.

Si hubiera condiciones de carrera, debería ver un mensaje en la consola, pero está vacío. Pero si uso los eventos clr como de costumbre, lo veo lleno de mensajes de advertencia. Por lo tanto, puedo concluir que es posible implementar eventos seguros para subprocesos en c #.

¿Qué piensas?


No creo que la pregunta esté restringida al tipo c # "evento". Eliminando esa restricción, ¿por qué no reinventar un poco la rueda y hacer algo en este sentido?

Aumentar el hilo del evento de forma segura - mejores prácticas

  • Posibilidad de sub / cancelar suscripción de cualquier hilo mientras se encuentre dentro de un aumento (condición de carrera eliminada)
  • Sobrecarga del operador para + = y - = a nivel de clase.
  • Delegado genérico definido por el llamante




events