2008-11-12 8 views
31

Algo que me confunde, pero nunca ha causado ningún problema ... la forma recomendada para distribuir un evento es el siguiente:Comprobando nulo antes de enviar el evento ... ¿es seguro para la ejecución de subprocesos?

public event EventHandler SomeEvent; 
... 
{ 
    .... 
    if(SomeEvent!=null)SomeEvent(); 
} 

En un entorno multi-hilo, ¿cómo funciona este código de garantía de que otro hilo no alterar la lista de invocación de SomeEvent entre el cheque de nulo y la invocación del evento?

Respuesta

53

Como usted señala, en que múltiples hilos pueden acceder SomeEvent al mismo tiempo, un hilo podría comprobar si SomeEvent es nula y determinar que no es Justo después de hacerlo, otro hilo podría eliminar al último delegado registrado de SomeEvent. Cuando el primer subproceso intenta aumentar SomeEvent, se lanzará una excepción. Una forma razonable de evitar esta situación es:

protected virtual void OnSomeEvent(EventArgs args) 
{ 
    EventHandler ev = SomeEvent; 
    if (ev != null) ev(this, args); 
} 

Esto funciona porque cada vez que se agrega un delegado o se elimina de un evento usando las implementaciones predeterminadas de los descriptores de acceso y quitar, el Delegate.Combine y Delegate.Remove estática métodos son usados. Cada uno de estos métodos devuelve una nueva instancia de un delegado, en lugar de modificar la que se le pasó.

Además, la asignación de una referencia de objeto en .NET es atomic, y las implementaciones predeterminadas de los accesos de agregar y quitar eventos son synchronised. Entonces, el código anterior tiene éxito copiando primero el delegado de multidifusión del evento a una variable temporal. Cualquier cambio en SomeEvent después de este punto no afectará la copia que haya realizado y almacenado. Por lo tanto, ahora puede comprobar de forma segura si los delegados se registraron y posteriormente invocarlos.

Tenga en cuenta que esta solución resuelve un problema de carrera, concretamente el de un controlador de eventos que es nulo cuando se invoca. No maneja el problema donde un controlador de eventos está extinto cuando se invoca, o un controlador de eventos se suscribe después de que se toma la copia.

Por ejemplo, si un controlador de eventos depende del estado que se destruye tan pronto como el controlador no está suscrito, esta solución podría invocar un código que no se puede ejecutar correctamente. Ver Eric Lippert's excellent blog entry para más detalles. También, vea this StackOverflow question and answers.

EDITAR: Si está utilizando C# 6.0, entonces Krzysztof's answer parece un buen camino a seguir.

+0

Tengo un problema con la declaración "Además, las asignaciones de referencias de objetos en .NET son seguras para hilos". ¿Seguro que te refieres a atómico? Por lo que yo sé, si el hilo A establece una referencia en la variable V, nada garantiza que el hilo B establecerá la referencia actualizada en la variable V, a menos que V sea volátil o se utilicen instrucciones de bloqueo al leer y escribir V. –

+0

Eso también significa que tu ejemplo esta roto Si el subproceso A agregó controladores de eventos a SomeEvent, y luego el subproceso B invoca SomeEvent, bien podría suceder que el subproceso B vea SomeEvent como nulo, a menos que SomeEvent se haya declarado como volátil –

+1

@Christophe, por "thread-safe", me refiero esa asignación de una referencia de objeto en un subproceso nunca se interrumpirá o se verá como inconsistente por otro subproceso. Como mencioné en mi actualización, definitivamente no es lo mismo que decir que el hilo A y el hilo B siempre tendrán la misma vista del evento. Todo lo que hace este ejemplo es evitar una condición de carrera específica, no todas las condiciones de carrera. – RoadWarrior

4

La forma recomendada es un poco diferente y utiliza un temporal de la siguiente manera:

EventHandler tmpEvent = SomeEvent; 
if (tmpEvent != null) 
{ 
    tmpEvent(); 
} 
+0

¿Entonces clona la lista de invocación? – spender

+0

sí - editaré con un ejemplo. –

+0

Alternativamente, puede llamar GetInvocationList a un temporal y ejecutar cada uno en orden. –

3

enfoque más seguro:

 

public class Test 
{ 
    private EventHandler myEvent; 
    private object eventLock = new object(); 

    private void OnMyEvent() 
    { 
     EventHandler handler; 

     lock(this.eventLock) 
     { 
      handler = this.myEvent; 
     } 
     if (handler != null) 
     { 
      handler(this, EventArgs.Empty); 
     } 
    } 

    public event MyEvent 
    { 
     add 
     { 
      lock(this.eventLock) 
      { 
       this.myEvent += value; 
      } 
     } 
     remove 
     { 
      lock(this.eventLock) 
      { 
       this.myEvent -= value; 
      } 
     } 

    } 
} 
 

-Bill

+1

Este es el enfoque correcto dado por Jon Skeet en http://www.yoda.arachsys.com/csharp/events.html –

+1

Ha sincronizado la asignación de controlador con un bloqueo, pero no buscará nulo después de salir el bloque sincronizado aún permite los mismos problemas? – user12345613

21

La forma más sencilla de eliminar este control es nula para asignar el manejador de sucesos a un delegado anónimo. La multa incurrida en muy poco y lo libera de todos los controles nulos, condiciones de carrera, etc.

public event EventHandler SomeEvent = delegate {};

pregunta relacionada: Is there a downside to adding an anonymous empty delegate on event declaration?

+0

¿Por qué esto no está marcado como la respuesta "oficial"? Esta es la * única * buena respuesta. – yfeldblum

+4

No estoy de acuerdo. Ese método parece un truco y no hace que los eventos de disparo sean "seguros" o confiables, solo resuelve el problema de la verificación nula. Por favor vea mi explicación en el hilo relacionado. –

0

me gustaría sugerir una ligera mejora a la respuesta de RoadWarrior por la utilización de una función de extensión para el manejador de sucesos:

public static class Extensions 
{ 
    public static void Raise(this EventHandler e, object sender, EventArgs args = null) 
    { 
     var e1 = e; 

     if (e1 != null) 
     { 
      if (args == null) 
       args = new EventArgs(); 

      e1(sender, args); 
     }     
    } 
    } 

Con esta ampliación en el alcance, los eventos se pueden plantear simplemente por:

clase SomeClass { evento público EventHandler MyEvent;

void SomeFunction() 
{ 
    // code ... 

    //--------------------------- 
    MyEvent.Raise(this); 
    //--------------------------- 
} 

}

14

En C# 6.0 se puede utilizar el operador monádico Null-condicional ?. para comprobar nula y provocar eventos en forma fácil y segura para los subprocesos.

SomeEvent?.Invoke(this, args); 

de Es seguro para subprocesos, ya que evalúa el lado izquierdo sólo una vez, y lo mantiene en una variable temporal. Puede leer más here en parte titulado Operadores Null-conditional.

+0

Probablemente regrese y marque esta como la respuesta correcta cuando actualicemos a vs2015 y he tenido la oportunidad de jugar con las nuevas funciones de idioma. – spender

+0

Agradable. Esto ahora se ve como 'EL' enfoque de facto en el futuro. – StuartLC

Cuestiones relacionadas