DarioSantarelli.Blog("UgiDotNet");

<sharing mode=”On” users=”*” />
posts - 176, comments - 105, trackbacks - 3

My Links

News


This is my personal blog. These postings are provided "AS IS" with no warranties, and confer no rights.




Tag Cloud

Archives

Post Categories

My English Blog

Reactive Extensions (Rx) e Weak events

Come molti sanno, il classico uso degli eventi in C# ha un effetto collaterale: se non viene gestita correttamente la strong reference che si forma tra l’oggetto che espone l’evento (EventSource) e gli oggetti che registrano gli event handlers (EventListeners), si rischia di generare dei memory leak. Infatti, se l’ EventSource ha un ciclo di vita più lungo rispetto agli EventListeners, questi ultimi, se non si deregistrano correttamente dall’evento, vengono tenuti in vita in memoria anche quando non ci sono più riferimenti attivi verso di essi e quindi non vengono reclamati dalla garbage collection fintantoché l’EventSource è attivo.
Nella letteratura esistono diverse tecniche per evitare questa tipologia di memory leak: questo articolo (“Weak Events in C#”) in particolare ne parla in maniera molto chiara e presenta vari approcci per implementare degli “weak event” (listener-side o source-side), ovvero degli eventi a cui un EventListener può registrarsi senza incorrere nel rischio di diventare un memory leak. Le soluzioni ormai più diffuse sfruttano la classe WeakReference , che permette di referenziare un oggetto in modo “debole”, ovvero mantenendolo comunque reclamabile dalla garbage collection.
Giusto per citare un caso noto, WPF introduce un design pattern chiamato “Weak Event”. Esso fondalmente si basa sull’implementazione concreta di un WeakEventManager, ovvero un “dispatcher” centralizzato per ogni evento che vogliamo gestire in modalità weak (molti degli aspetti del data binding di WPF sono implementati tramite weak event). In questo pattern, poi, tutti i listener del weak event devono implementare l’interfaccia IWeakEventListener e quindi registrarsi/deregistrarsi all’evento passando per i metodi statici AddListener() e RemoveLister() che il WeakEventManager mette a disposizione. Come si può intuire, questa soluzione introduce dell’infrastruttura che non sempre è semplice gestire, soprattutto in applicazioni esistenti che fanno un uso complesso e massivo di eventi.
Pochi giorni fa ho provato quindi ad affrontare la questione attraverso il Reactive Extensions (Rx) framework e devo dire che le soluzioni che si possono trovare sono alquanto iteressanti e flessibili. Vorrei mostrarne una in particolare.

Supponiamo di avere una classe EventSource che espone un evento pubblico Event.

public class EventSource 
{
public event EventHandler Event;

public void FireEvent()
{
EventHandler handler = Event;
if (handler != null) handler(this, EventArgs.Empty);
}

public int GetEventListenersCount()
{
return Event.GetInvocationList().Count();
}
}

 

Per fini di test esponiamo anche un metodo FireEvent() che scatena l’evento ed un metodo GetEventListenersCount() che ci restituisce il numero di listener correntemente registrati all’evento. Creiamo quindi uno StrongEventListener, ovvero un listener che crea una strong reference all’EventSource tramite la classica sintassi di registrazione ad un evento. Poichè questo oggetto non effettua esplicitamente la deregistrazione, diventa un potenziale memory leak.

public class StrongEventListener 
{
public StrongEventListener(EventSource eventSource)
{
eventSource.Event += HandleEvent;
}

private void HandleEvent(object sender, EventArgs eventArgs)
{
Console.WriteLine("StrongEventListener> Event Received!");
}
}


Per riuscire a implementare la corrispettiva versione weak (WeakEventListener) in modo semplice e veloce è possibile sfruttare le Reactive Extensions (Rx).
Anzitutto partiamo dal risultato che vorremmo ottenere, ovvero un oggetto che possa registrarsi all’evento del nostro EventSource in modalità weak, ovvero senza correre il rischio di diventare un potenziale memory leak:

public class WeakEventListener 
{
public WeakEventListener(EventSource eventSource)
{
eventSource.ObservableEvent()
.WeakSubscribe(this, (me, e) => me.HandleEvent(e.Sender, e.EventArgs));
}

private void HandleEvent(object sender, EventArgs eventArgs)
{
Console.WriteLine("WeakEventListener> Event Received!");
}
}


Per ottenere questo risultato le fasi da seguire sono due:

1) Anzitutto, per leggibilità creiamo un extension method di EventSource che ne converte l’evento in un IObservable grazie al classico metodo Observable.FromEventPattern(...) di Rx. Dietro le quinte viene creato e restituito un oggetto che sarà in grado di gestire la registrazione e la deregistrazione all’evento, aggiungendo e rimuovendo gli event handlers (strong references) verso l’istanza di EventSource in esame. 

public static IObservable<EventPattern<EventArgs>> ObservableEvent(this EventSource eventSource) 
{
return Observable.FromEventPattern<EventHandler, EventArgs>(h => (s, e) => h(s, e),
h => eventSource.Event += h,
h => eventSource.Event -= h);
}


2) Creiamo un extension method di IObservable<T> che chiamiamo WeakSubscribe. Esso ci permette di passare come argomento un oggetto subscriber ed una action da eseguire a fronte di una notifica (onNext) da parte della sequenza osservabile, in modo tale che il subscriber possa essere notificato tramite l’action in modalità “weak”, ovvero senza mantenere degli strong reference con la sequenza osservabile.

public static IDisposable WeakSubscribe<T, TSubscriber>(this IObservable<T> observable,
TSubscriber subscriber, Action<TSubscriber, T> action)
where TSubscriber : class
{
if (action.Target != null) throw new ArgumentException("action target must NOT refer a class instance, " +
"in order to avoid a strong reference to target");
var subscriberWeakReference = new WeakReference(subscriber);
IDisposable subscription = null;
Action<T> onNext = item =>
{
var subscriberObj = subscriberWeakReference.Target as TSubscriber;
if (subscriberObj != null) action(subscriberObj, item);
else subscription.Dispose();
};
subscription = observable.Subscribe(onNext);
return subscription;
}


Qui il gioco si fa interessante ed anche abbastanza complicato: come si osserva, anzitutto il prerequisito è che venga mantenuta una weak reference sul subscriber tramite una WeakReference. Dopodiché, con una lambda definiamo l’onNext di un AnonymousObserver della sequenza osservabile, il quale ad ogni notifica controlla se il subscriber è ancora vivo (subscriberWeakReference.Target != null). In caso positivo, viene eseguita l’action che fa da “passacarte” tra l’observer ed il subscriber, altrimenti viene chiamata la Dispose() sulla subscription dell’observer (il che nel nostro esempio si traduce nella deregistrazione dall’evento dell’EventSource (eventSource.Event -= h)).

Ora arriva il trucco: per evitare che il subscriber diventi un potenziale memory leak, action.Target non deve riferire direttamente l’istanza del subscriber, altrimenti significherebbe che il chiamante del nostro metodo sta passando il puntatore this su action.Target (in altre parole, action.Target == null significa che il delegato action invoca un metodo statico e non di istanza). La action quindi deve essere una lambda che chiama metodi di istanza del subscriber tramite il riferimento che essa stessa fornisce come parametro, piuttosto che catturare il riferimento implicito a this:

.WeakSubscribe(this, (me, e) => this.HandleEvent(e.Sender, e.EventArgs)); // –> FAIL (ArgumentException)
.WeakSubscribe(this, (me, e) => me.HandleEvent(e.Sender, e.EventArgs)); // –> OK

Questo controllo ci garantisce che la lambda che poi specifichiamo sull’onNext non generi una strong reference verso il subscriber tramite l’action. Ciò renderebbe il subscriber un potenziale memory leak. Capiamo meglio il perché: nel nostro esempio, avendo usato un Observable.FromEventPattern, quando chiamiamo la observable.Subscribe(onNext) scateniamo internamente la registrazione all’evento dell’EventSource (h => eventSource.Event += h). h è un EventHandler che tramite una closure interna di Rx crea una strong reference tra l’EventSource e la onNext. La onNext a sua volta mantiene uno strong reference verso la action. Quindi, a cascata, se l’action che usiamo nella lambda fosse un metodo di istanza del subscriber, l’istanza stessa del subscriber diventerebbe un potenziale memory leak poiché manterrebbe uno strong reference attivo nella subscription.

Conclusioni

Il grande vantaggio di questa soluzione è che può introdurre in progetti esistenti una semplice gestione thread-safe di weak events al costo di una banale AddReference a System.Reactive.dll. Infatti, a differenza del “Weak Event Pattern”, questo approccio ha praticamente impatto zero sull’ infrastruttura.
L’unico svantaggio invece è che la subscription.Dispose() avviene eventualmente sull’ onNext della sequenza osservabile, il che tradotto nel nostro esempio significa che la deregistrazione dei delegati dall’evento dell’EventSource avviene eventualmente SOLO a fronte dello scatenare tale evento. Di conseguenza, la onNext che usiamo per fare observable.Subscribe(onNext) potrebbe confluire in dei memory leak fintantoché l’evento non viene scatenato dall’EventSource (come ho provato a dimostrare con lo Unit test qua sotto). I nostri WeakEventListener invece vivono felicemente il loro ciclo di vita sicuri di non diventare dei memory leak, poiché ovviamente non sono loro a registrasi direttamente all’evento dell’EventSource
In conclusione, prima di considerare questa implementazione e comunque in generale, cerchiamo sempre di valutare attentamente qual’è il ciclo di vita degli EventListener rispetto agli EventSource :).

[TestMethod] 
public void WeakEvent_With_Rx()
{
var eventSource = new EventSource();
var strongListener = new StrongEventListener(eventSource);
var weakListener = new WeakEventListener(eventSource);

Assert.AreEqual(2, eventSource.GetEventListenersCount());

eventSource.FireEvent();
// OUTPUT:
// StrongEventListener> EventReceived!
// WeakEventListener> EventReceived!

strongListener = null;
weakListener = null;

GC.Collect();
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();

 
   Assert.AreEqual(2, eventSource.GetEventListenersCount()); 
// 2 Memory Leak
// - StrongEventListener has NOT been garbage collected (Obviously)
// - WeakEventListener has been garbage collected!
// – onNext action delegate is still a memory leak



eventSource.FireEvent();
// OUTPUT:
// StrongEventListener> EventReceived!

Assert.AreEqual(1, eventSource.GetEventListenersCount());
// 1 Memory Leak (StrongEventListener (Obviously))
}


HTH

Print | posted on Sunday, March 25, 2012 4:15 PM | Filed Under [ .NET ]

Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET