c# - remarks - Subir el hilo del evento de forma segura: las mejores prácticas
remarks/> (10)
Para generar un evento usamos un método OnEventName como este:
protected virtual void OnSomethingHappened(EventArgs e)
{
EventHandler handler = SomethingHappened;
if (handler != null)
{
handler(this, e);
}
}
Pero, ¿cuál es la diferencia con este?
protected virtual void OnSomethingHappened(EventArgs e)
{
if (SomethingHappened!= null)
{
SomethingHappened(this, e);
}
}
Al parecer, el primero es seguro para subprocesos, pero ¿por qué y cómo?
No es necesario comenzar un nuevo hilo?
Declara tu evento así para obtener seguridad de hilo:
public event EventHandler<MyEventArgs> SomethingHappened = delegate{};
E invocarlo así:
protected virtual void OnSomethingHappened(MyEventArgs e)
{
SomethingHappened(this, e);
}
Aunque el método ya no es necesario ...
Depende de lo que quieras decir con thread-safe. Si su definición solo incluye la prevención de NullReferenceException
entonces el primer ejemplo es más seguro. Sin embargo, si usa una definición más estricta en la que los manejadores de eventos se deben invocar si existen, ninguno de los dos es seguro. La razón tiene que ver con las complejidades del modelo de memoria y las barreras. Podría ser que, de hecho, hay controladores de eventos encadenados al delegado, pero el hilo siempre lee la referencia como nula. La forma correcta de corregir ambos es crear una barrera de memoria explícita en el punto en que la referencia del delegado se captura en una variable local. Hay varias maneras de hacer esto.
- Use la palabra clave de
lock
(o cualquier mecanismo de sincronización). - Use la palabra clave
volatile
en la variable de evento. - Use
Thread.MemoryBarrier
.
A pesar del incómodo problema de alcance que le impide hacer el inicializador de una línea, prefiero el método de lock
.
protected virtual void OnSomethingHappened(EventArgs e)
{
EventHandler handler;
lock (this)
{
handler = SomethingHappened;
}
if (handler != null)
{
handler(this, e);
}
}
Es importante tener en cuenta que, en este caso específico, el problema de la barrera de la memoria probablemente sea irrelevante, ya que es poco probable que las lecturas de variables se eliminen fuera de las llamadas a métodos. Pero no hay garantías, especialmente si el compilador decide alinear el método.
Desde C # 6.0, puede usar el operador monadic Null-conditional ?.
para comprobar nulo y generar eventos de manera fácil y segura para la ejecución de subprocesos.
SomethingHappened?.Invoke(this, args);
Es seguro para subprocesos porque evalúa el lado izquierdo solo una vez y lo mantiene en una variable temporal. Puede leer más here en la parte titulada Operadores Null-conditional.
Actualización: en realidad, la Actualización 2 para Visual Studio 2015 ahora contiene refactorización para simplificar las invocaciones de delegados que terminarán exactamente con este tipo de notación. Puedes leer sobre esto en este announcement .
En realidad, el primero es seguro para subprocesos, pero el segundo no. El problema con el segundo es que el delegado SomethingHappened podría cambiar a nulo entre la verificación nula y la invocación. Para obtener una explicación más completa, consulte http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx .
En realidad, no, el segundo ejemplo no se considera seguro para subprocesos. El evento SomethingHappened podría evaluarse como no nulo en el condicional, y luego ser nulo cuando se invoca. Es una condición de carrera clásica.
Guardo este fragmento como una referencia para el acceso seguro a eventos multiproceso para la configuración y la activación:
/// <summary>
/// Lock for SomeEvent delegate access.
/// </summary>
private readonly object someEventLock = new object();
/// <summary>
/// Delegate variable backing the SomeEvent event.
/// </summary>
private EventHandler<EventArgs> someEvent;
/// <summary>
/// Description for the event.
/// </summary>
public event EventHandler<EventArgs> SomeEvent
{
add
{
lock (this.someEventLock)
{
this.someEvent += value;
}
}
remove
{
lock (this.someEventLock)
{
this.someEvent -= value;
}
}
}
/// <summary>
/// Raises the OnSomeEvent event.
/// </summary>
public void RaiseEvent()
{
this.OnSomeEvent(EventArgs.Empty);
}
/// <summary>
/// Raises the SomeEvent event.
/// </summary>
/// <param name="e">The event arguments.</param>
protected virtual void OnSomeEvent(EventArgs e)
{
EventHandler<EventArgs> handler;
lock (this.someEventLock)
{
handler = this.someEvent;
}
if (handler != null)
{
handler(this, e);
}
}
Hay una pequeña posibilidad de que SomethingHappened
sea null
después de la comprobación nula pero antes de la invocación. Sin embargo, MulticastDelagate
s son inmutables, por lo que si primero asigna una variable, marca nulo contra la variable e invoca a través de ella, está a salvo de ese escenario (autoenchufe: escribí una publicación en el blog sobre esto hace un tiempo).
Sin embargo, hay un reverso de la moneda; si usa el enfoque de la variable de temperatura, su código está protegido contra NullReferenceException
, pero podría ser que el evento invocará los detectores de eventos después de que se hayan separado del evento . Eso es algo de lo que hay que ocuparse de la manera más elegante posible.
Para evitar esto, tengo un método de extensión que a veces uso:
public static class EventHandlerExtensions
{
public static void SafeInvoke<T>(this EventHandler<T> evt, object sender, T e) where T : EventArgs
{
if (evt != null)
{
evt(sender, e);
}
}
}
Usando ese método, puedes invocar los eventos de esta manera:
protected void OnSomeEvent(EventArgs e)
{
SomeEvent.SafeInvoke(this, e);
}
Para .NET 4.5, es mejor usar Volatile.Read
para asignar una variable de temperatura.
protected virtual void OnSomethingHappened(EventArgs e)
{
EventHandler handler = Volatile.Read(ref SomethingHappened);
if (handler != null)
{
handler(this, e);
}
}
Actualizar:
Se explica en este artículo: http://msdn.microsoft.com/en-us/magazine/jj883956.aspx . Además, se explicó en la Cuarta edición de "CLR via C #".
La idea principal es que el compilador JIT puede optimizar su código y eliminar la variable temporal local. Entonces este código:
protected virtual void OnSomethingHappened(EventArgs e)
{
EventHandler handler = SomethingHappened;
if (handler != null)
{
handler(this, e);
}
}
se compilará en esto:
protected virtual void OnSomethingHappened(EventArgs e)
{
if (SomethingHappened != null)
{
SomethingHappened(this, e);
}
}
Esto sucede en ciertas circunstancias especiales, sin embargo, puede suceder.
Para que cualquiera de estos sea seguro para subprocesos, está asumiendo que todos los objetos que se suscriben al evento también son seguros para subprocesos.
Traté de la respuesta de con:
- Posibilidad de sub / darse de baja de cualquier tema dentro de un aumento (condición de carrera eliminada)
- El operador se sobrecarga para + = y - = en el nivel de clase
Delegados definidores de llamadas genéricas
public class ThreadSafeEventDispatcher<T> where T : class { readonly object _lock = new object(); private class RemovableDelegate { public readonly T Delegate; public bool RemovedDuringRaise; public RemovableDelegate(T @delegate) { Delegate = @delegate; } }; List<RemovableDelegate> _delegates = new List<RemovableDelegate>(); Int32 _raisers; // indicate whether the event is being raised // Raises the Event public void Raise(Func<T, bool> raiser) { try { List<RemovableDelegate> raisingDelegates; lock (_lock) { raisingDelegates = new List<RemovableDelegate>(_delegates); _raisers++; } foreach (RemovableDelegate d in raisingDelegates) { lock (_lock) if (d.RemovedDuringRaise) continue; raiser(d.Delegate); // Could use return value here to stop. } } finally { lock (_lock) _raisers--; } } // Override + so that += works like events. // Adds are not recognized for any event currently being raised. // public static ThreadSafeEventDispatcher<T> operator +(ThreadSafeEventDispatcher<T> tsd, T @delegate) { lock (tsd._lock) if (!tsd._delegates.Any(d => d.Delegate == @delegate)) tsd._delegates.Add(new RemovableDelegate(@delegate)); return tsd; } // Override - so that -= works like events. // Removes are recongized immediately, even for any event current being raised. // public static ThreadSafeEventDispatcher<T> operator -(ThreadSafeEventDispatcher<T> tsd, T @delegate) { lock (tsd._lock) { int index = tsd._delegates .FindIndex(h => h.Delegate == @delegate); if (index >= 0) { if (tsd._raisers > 0) tsd._delegates[index].RemovedDuringRaise = true; // let raiser know its gone tsd._delegates.RemoveAt(index); // okay to remove, raiser has a list copy } } return tsd; } }
Uso:
class SomeClass
{
// Define an event including signature
public ThreadSafeEventDispatcher<Func<SomeClass, bool>> OnSomeEvent =
new ThreadSafeEventDispatcher<Func<SomeClass, bool>>();
void SomeMethod()
{
OnSomeEvent += HandleEvent; // subscribe
OnSomeEvent.Raise(e => e(this)); // raise
}
public bool HandleEvent(SomeClass someClass)
{
return true;
}
}
¿Algún problema importante con este enfoque?
El código fue brevemente probado y editado un poco en la inserción.
Confirme previamente que List <> no es una gran opción si hay muchos elementos.