Programming Languages Hacks

Importanti regole per linguaggi di programmazione rilevanti come Java, C, C++, C#…

  • Subscribe

  • Lettori

    I miei lettori abituali

  • Twitter

Weak Delegate: gestire Eventi senza causare Memory Leak

Posted by Ricibald on December 14th, 2009

Uno dei motivi principali di memory leak è il riferimento circolare tra oggetti, come già osservato nel precedente post.

Un’istanza particolare di questo problema si trova nella gestione degli eventi. Consideriamo questo scenario: la window2 (subscriber) registra il proprio handler all’evento TextChanged della window1 padre (publisher). Ci aspetteremmo che la window2 venga deallocata quando chiusa ma questo non succederà poiché la window1 mantiene uno strong reference verso la window2 mediante la registrazione dell’handler.

La window1 in realtà detiene l’ownership di window2 poiché è la finestra padre, ma questa relazione viene già mantenuta mediante la lista Children, non tramite l’handler.
Il punto è quindi che, mentre la cancellazione della lista viene gestita dallo stesso framework grafico che quindi garantisce uno stato corretto per la deallocazione, risulta invece nostra responsabilità gestire la deferenziazione di altri strong reference mantenuti. Questo discorso non si applica quindi solo agli event handler ma anche a data binding e a command, i quali internamente mantengono strong reference.

Una soluzione consiste nel deregistrare gli handler (o databinding o command) al momento della close della window2, ma risulta una soluzione fragile: si presta facilmente a errori durante lo sviluppo.

Una soluzione migliore sarebbe registrare un “weak delegate”: in questo modo il publisher non vincolerebbe il ciclo di vita dei subscriber.

Ma come fare a creare un weak delegate? Il trucco è in sostanza questo: dobbiamo rimpiazzare il “delegate originale” con un suo “delegate proxy”, il quale invoca il delegate originale se il target del delegato non è stato deallocato. Per fare questo quindi il delegate proxy deve mantenere un weak reference verso il delegate originale. La soluzione concettuale è espressa in queste poche righe di codice:

public static EventHandler ToWeak(EventHandler eventHandler)
{
    var weakAction = new WeakReference(eventHandler);
    var handler = new EventHandler((sender, e) =>
    {
        var target = weakAction.Target as EventHandler;
        if (target != null) target(sender, e);
    });
    return handler;
}

Combinando questo con gli extension methods possiamo quindi ottenere un fantastico metodo “ToWeak” che trasforma un delegate standard in un “weak delegate”:

    public static class ExtensionMethods
    {
        public static EventHandler ToWeak(this EventHandler eventHandler)
        {
            var weakAction = new WeakReference(eventHandler);
            var handler = new EventHandler((sender, e) =>
            {
                var target = weakAction.Target as EventHandler;
                if (target != null) target(sender, e);
            });
            return handler;
        }

        public static EventHandler<TEventArgs> ToWeak<TEventArgs>(this EventHandler<TEventArgs> eventHandler)
            where TEventArgs : System.EventArgs
        {
            var weakAction = new WeakReference(eventHandler);
            var handler = new EventHandler<TEventArgs>((sender, e) =>
            {
                var target = weakAction.Target as EventHandler<TEventArgs>;
                if (target != null) target(sender, e);
            });
            return handler;
        }
    }

    class Subscriber
    {
        public void Handler(object sender, EventArgs e)
        {
            Console.WriteLine("Handler called");
        }

        ~Subscriber()
        {
            Console.WriteLine("Subscriber Cleaned");
        }
    }

    class Publisher
    {
        public event EventHandler Click;
        public string Id { get; private set; }

        public Publisher(string id)
        {
            this.Id = id;
        }

        ~Publisher()
        {
            Console.WriteLine("Publisher Cleaned");
        }

        public void OnClick()
        {
            if(Click != null)
            {
                Click(this, new EventArgs());
            }
        }
    }

    class Program
    {
        public static void Main()
        {
            Console.WriteLine("+ TEST 1: WITHOUT WakeEventHandler");
            RunTestWithWakeFlag(false);

            Console.WriteLine("+ TEST 2: WITH WakeEventHandler");
            RunTestWithWakeFlag(true);

            Console.ReadLine();
        }

        public static void RunTestWithWakeFlag(bool isWake)
        {
            waitAndWrite("+--- ***Subscriber Creation***");
            var subscriber = new Subscriber();

            waitAndWrite("+------ Holding the Subscriber's Handler");
            EventHandler h = createEventHandler(subscriber.Handler, isWake);

            waitAndWrite("+------ Raising Subscriber Event");
            h(null, EventArgs.Empty);

            waitAndWrite("+------ Subscriber not yet used by application. GC called\r\n+------ [!!!LEAK HERE IF NOT WEAK!!!] (CLASSES MANTAINES IN LIFE ALL CLASSES'S DELEGATE)");
            subscriber = null;
            garbageCollect();

            waitAndWrite("+------ Raising Subscriber Event Again");
            h(null, EventArgs.Empty);

            waitAndWrite("+------ Handler not yet used by application. GC called");
            h = null;
            garbageCollect();

            waitAndWrite("+--- ***Publisher Creation (12345)***");
            var publisher12345 = new Publisher("12345");

            waitAndWrite("+------ Publisher Registration Anonymous Event");
            publisher12345.Click +=
                createEventHandler((s, a) => Console.WriteLine(String.Format("Anonymous Subscriber Called (subscribed to {0})", publisher12345.Id)), isWake);

            waitAndWrite("+------ Raising Publisher's Events");
            publisher12345.OnClick();

            waitAndWrite("+------ Publisher not yet used by application. GC called");
            publisher12345 = null;
            garbageCollect();

            waitAndWrite("+--- ***Publisher Creation (67890)***");
            var publisher67890 = new Publisher("67890");

            waitAndWrite("+------ Publisher Registration Event in other Class");
            var subscriber67890 = new Subscriber();
            publisher67890.Click += createEventHandler(subscriber67890.Handler, isWake);

            waitAndWrite("+------ Raising Publisher's Events");
            publisher67890.OnClick();

            waitAndWrite("+------ Subscriber not yet used by application. GC called\r\n+------ [!!!LEAK HERE IF NOT WEAK!!!] (PUBLISHER MANTAINES IN LIFE ALL ITS SUBSCRIBERS)");
            subscriber67890 = null;
            garbageCollect();

            waitAndWrite("+------ Publisher not yet used by application. GC called");
            publisher67890 = null;
            garbageCollect();

            waitAndWrite("+--- End");
        }

        private static void waitAndWrite(string text)
        {
            Console.ReadLine();
            Console.WriteLine(text);
        }

        private static void garbageCollect()
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
        }

        private static EventHandler createEventHandler(EventHandler action, bool isWake)
        {
            return isWake ? action.ToWeak() : action;
        }
    }

L’output prodotto a video dai test è il seguente:

+ TEST 1: WITHOUT WakeEventHandler

+--- ***Subscriber Creation***

+------ Holding the Subscriber's Handler

+------ Raising Subscriber Event
Handler called

+------ Subscriber not yet used by application. GC called
+------ [!!!LEAK HERE IF NOT WEAK!!!] (CLASSES MANTAINES IN LIFE ALL CLASSES'S D
ELEGATE)

+------ Raising Subscriber Event Again
Handler called

+------ Handler not yet used by application. GC called
Subscriber Cleaned

+--- ***Publisher Creation (12345)***

+------ Publisher Registration Anonymous Event

+------ Raising Publisher's Events
Anonymous Subscriber Called (subscribed to 12345)

+------ Publisher not yet used by application. GC called
Publisher Cleaned

+--- ***Publisher Creation (67890)***

+------ Publisher Registration Event in other Class

+------ Raising Publisher's Events
Handler called

+------ Subscriber not yet used by application. GC called
+------ [!!!LEAK HERE IF NOT WEAK!!!] (PUBLISHER MANTAINES IN LIFE ALL ITS SUBSC
RIBERS)

+------ Publisher not yet used by application. GC called
Subscriber Cleaned
Publisher Cleaned

+--- End

+ TEST 2: WITH WakeEventHandler

+--- ***Subscriber Creation***

+------ Holding the Subscriber's Handler

+------ Raising Subscriber Event
Handler called

+------ Subscriber not yet used by application. GC called
+------ [!!!LEAK HERE IF NOT WEAK!!!] (CLASSES MANTAINES IN LIFE ALL CLASSES'S D
ELEGATE)
Subscriber Cleaned

+------ Raising Subscriber Event Again

+------ Handler not yet used by application. GC called
WeakEventHandler Cleaned

+--- ***Publisher Creation (12345)***

+------ Publisher Registration Anonymous Event

+------ Raising Publisher's Events
Anonymous Subscriber Called (subscribed to 12345)

+------ Publisher not yet used by application. GC called
WeakEventHandler Cleaned
Publisher Cleaned

+--- ***Publisher Creation (67890)***

+------ Publisher Registration Event in other Class

+------ Raising Publisher's Events
Handler called

+------ Subscriber not yet used by application. GC called
+------ [!!!LEAK HERE IF NOT WEAK!!!] (PUBLISHER MANTAINES IN LIFE ALL ITS SUBSC
RIBERS)
Subscriber Cleaned

+------ Publisher not yet used by application. GC called
WeakEventHandler Cleaned
Publisher Cleaned

+--- End

Si noti un aspetto importante: se abbiamo 99 subscriber, la chiusura delle 99 window dei subscriber non deregistra i 99 handler dell’evento nel publisher, semplicemente i 99 handler ora punteranno a un reference ormai deallocato, ma continueranno a esistere. Perciò un leggerissimo memory leak permane e a livello computazionale il sistema continuerà ancora a gestire 99 handler. Ma la struttura dei subscriber viene deallocata correttamente ed è ciò che conta, poiché sarà il memory leak “vero” da gestire.

Infine bisogna notare che l’implementazione può essere notevolmente raffinata e resa generica come indicato nell’articolo di Greg Schechter applicando reflection o lambda expression, ma questo può causare rallentamenti di performance che secondo me devono essere giustificati dalla necessità effettiva di rendere l’approccio generico.

Leave a Reply

You must be logged in to post a comment.