236 votos

C# Eventos y el Hilo de Seguridad

Frecuentemente oigo/leer los siguientes consejos:

Realice siempre una copia de un evento antes de que usted lo revise null y el fuego. Esto eliminará un problema potencial con el roscado donde el evento se convierte null a la ubicación, justo entre el lugar donde comprobar el valor null y donde se desencadena 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: acabo de leer acerca de las optimizaciones que esto también podría requerir que el miembro del evento para ser volátil, pero a Jon Skeet de los estados en su respuesta que el CLR no optimizar lejos de la copia.

Pero mientras tanto, con el fin de que esta cuestión se le ocurre, otro hilo debe de 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 se ejecuta después de que el autor ha cancelado su suscripción, sin embargo, ellos simplemente no suscritas específicamente para evitar que eso ocurra. Sin duda, de lo que realmente se necesita es un evento personalizado aplicación con la adecuada sincronización en la add y remove descriptores de acceso. Y además, está el problema de la posible interbloqueos si se mantiene un bloqueo, mientras que un evento se dispara.

Así es esto de la Carga de Culto de la Programación? Parece de esa manera - un montón de gente se debe de tomar este paso para proteger el código de varios subprocesos, cuando en realidad a mí me parece que los eventos requieren mucha más atención de la que esta antes de que puedan ser utilizados como parte de un diseño multihilo. En consecuencia, las personas que no están tomando que el cuidado adicional podría ignorar este consejo - simplemente, no es un problema para los programas single-threaded, y de hecho, dada la ausencia de volatile en la mayoría del código de ejemplo, el consejo puede no tener ningún efecto en absoluto.

(Y no es que sea mucho más sencillo simplemente asignar el vacío delegate { } en la declaración del miembro, de modo que usted nunca tendrá que comprobar null en el primer lugar?)

Actualizado: En caso de que no era clara, me hizo comprender la intención de los consejos - para evitar una referencia nula excepción en todas las circunstancias. Mi punto es que este particular referencia nula excepción sólo puede producirse si otro hilo es eliminado del evento, y la única razón para hacerlo es asegurarse de que no hay más que las llamadas serán recibidas a través de ese evento, que claramente NO es alcanzado por esta técnica. Usted estaría ocultando una condición de carrera - sería mejor para revelar! Que la excepción nula ayuda a detectar un abuso de su componente. Si desea que el componente a ser protegidos del abuso, que podrían seguir el ejemplo de WPF - almacenar el IDENTIFICADOR de subproceso en el constructor y, a continuación, lanzar una excepción si otro thread intenta interactuar directamente con el componente. O bien implementar una verdadera thread-safe componente (no es una tarea fácil).

Así que sostienen que el mero hecho de hacer esta copia/check modismo es la carga de culto de programación, añadiendo el desorden y el ruido de su código. Para realmente proteger contra otros hilos requiere mucho más trabajo.

Actualización en respuesta a Eric Lippert posts en el blog:

Así que hay una gran cosa que se me había perdido acerca de los controladores de eventos: "manejadores de eventos son necesarios para ser eficaz en la cara de ser llamado, incluso después de que el evento ha sido cancelado la suscripción", y, obviamente, por lo tanto sólo necesita preocuparse por la posibilidad de que el delegado de evento se null. Es que el requisito de que los manejadores de evento documentado en ninguna parte?

Y así: "Hay otras formas de resolver este problema; por ejemplo, la inicialización del controlador de tener una acción vacía que nunca se quita. Pero haciendo una verificación null es el patrón estándar."

Así que el único fragmento de mi pregunta es, ¿por qué es explícito-null-comprobar el "modelo estándar"? La alternativa, la asignación de los vacíos delegado, sólo requiere = delegate {} será agregado a la declaración de evento, y de esta forma se elimina esos pequeños montones de apestoso de la ceremonia de entrega de cada lugar donde se produce el evento. Sería fácil para asegurarse de que el vacío delegado es barato para crear una instancia. O soy yo que aún falta algo?

Seguramente debe ser que (como Jon Skeet sugerido), esto es sólo .NET 1.x consejo que no ha muerto, como se debería haber hecho en el 2005?

100voto

Jon Skeet Puntos 692016

El JIT no es permitido para realizar la optimización de las que estamos hablando en la primera parte, a causa de la condición. Yo sé que esto fue planteado como un fantasma hace un tiempo, pero no es válido. (Lo he comprobado con Joe Duffy o Vance Morrison hace un tiempo; no recuerdo cuál.)

Sin el modificador volatile es posible que la copia local tomado estará fuera de fecha, pero eso es todo. No causar NullReferenceException.

Y sí, ciertamente, hay una condición de carrera -, pero no siempre será así. Supongamos que acabamos de cambiar el código:

TheEvent(this, EventArgs.Empty);

Ahora supongamos que la lista de invocación para que el delegado tiene 1000 entradas. Es perfectamente posible que la acción en el inicio de la lista se han ejecutado antes de que otro hilo se da de baja un controlador cerca del final de la lista. Sin embargo, el controlador todavía va a ser ejecutada, porque va a ser una nueva lista. (Los delegados son inmutables.) Por lo que puedo ver es inevitable.

El uso de un vacío delegado ciertamente evita la nulidad de verificación, pero no corrige la condición de carrera. Asimismo, no se garantiza que usted "ver" siempre el último valor de la variable.

82voto

Eric Lippert Puntos 300275

Esta pregunta surgió en una discusión interna de un tiempo; he sido la intención de este blog por algún tiempo ahora.

Mi post sobre el tema aquí:

Eventos y Carreras

52voto

JP Alioto Puntos 33482

Veo un montón de gente que va hacia el método de extensión de hacer esto ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

Que se le da mejor la sintaxis para elevar el evento ...

MyEvent.Raise( this, new MyEventArgs() );

Y también elimina la copia local desde que es capturado en el método de tiempo de llamada.

34voto

Daniel Fortunov Puntos 12044

"¿Por qué es explícito-null-compruebe el 'modelo estándar'?"

Sospecho que la razón para esto podría ser que el nulo cheque es más eficiente.

Si siempre la suscripción de un vacío delegado para tus eventos cuando son creados, habrá algunos gastos generales:

  • Costo de construcción de la vacía delegado.
  • Costo de la construcción de un delegado de la cadena que lo contiene.
  • Costo de invocar el sentido de delegar cada vez que se produce el evento.

(Tenga en cuenta que los controles de interfaz de usuario tienen a menudo un gran número de eventos, la mayoría de los cuales nunca hizo. La necesidad de crear un muñeco de abonado para cada evento y, a continuación, invocar es probable que sea un importante impacto en el rendimiento.)

Hice algunas superficial de pruebas de rendimiento para ver el impacto de la suscribirse-vacío-delegado de enfoque, 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 uno de los suscriptores (común para los controles de interfaz de usuario, donde los acontecimientos son abundantes), el evento de pre-inicializado con un vacío delegado es notablemente más lento (más de 50 millones de iteraciones...)

Para obtener más información y código fuente, visite esta entrada del blog .NET Caso de invocación de la seguridad de los subprocesos que he publicado justo el día antes de que esta pregunta se la hicieron (!)

(Mi configuración de la prueba puede ser errónea, así que siéntete libre de descargar el código fuente e inspeccionar a ti mismo. Cualquier comentario se agradece mucho.)

11voto

Chuck Savage Puntos 6106

Yo realmente disfrutamos de esta lectura no! Aunque la necesito para trabajar con C# característica de llamada de eventos!

¿Por qué no solucionar este problema en el compilador? Sé que hay MS personas que leen estos mensajes, así que por favor no se llama esto!

1 - el Nulo tema) ¿por Qué no hacer eventos de ser .Empty en lugar de null en el primer lugar? ¿Cuántas líneas de código, sería guardada por nulo cheque o tener a un palo = delegate {} en la declaración? Dejar que el compilador de la manija de la caja Vacía, es decir, no hacer nada! Si todo es importante para el creador del evento, se puede comprobar .Empty y hacer lo que sea que cuidado con él! De lo contrario, todas las nulos cheques / delegado agrega son hacks de todo el problema!

Sinceramente estoy cansado de tener que hacer esto con cada evento - también conocido como el código repetitivo!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

2 - la condición de carrera problema) he leído Eric entrada en el blog, estoy de acuerdo en que el H (controlador) debe manejar cuando se elimina referencias a sí mismo, pero no el caso de ser hechos inmutables/thread safe? Es decir, establecer un bloqueo de la bandera de su creación, de manera que cuando se le llama, se bloquea toda la suscripción y des-suscribirse a él, mientras que su ejecución?

Conclusión,

No son los días modernos de los idiomas supone para resolver problemas como estos para nosotros?

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X