Tag Archives: weak event

Weak events en C#, suite

Il y a quelques années, j’ai blogué à propos d’une implémentation générique du pattern “weak event” en C#. Le but était de pallier les problèmes de fuites mémoire liés aux évènements quand on oublie de s’en désabonner. L’implémentation était basée sur l’utilisation de références faibles sur les abonnés, de façon à éviter d’empêcher qu’ils soient libérés par le garbage collector.

Ma solution initiale était plus une preuve de concept qu’autre chose, et avait un sérieux problème de performance, dû à l’utilisation de DynamicInvoke à chaque fois que l’évènement était déclenché. Au fil des années, j’ai revisité le problème des “weak events” plusieurs fois, en apportant quelques améliorations à chaque fois, et j’ai maintenant une implémentation qui devrait être suffisamment performante pour la plupart des cas d’utilisation. L’API publique est similaire à celle de ma première solution. En gros, au lieu d’écrire un évènement comme ceci :

public event EventHandler<MyEventArgs> MyEvent;

On l’écrit comme ceci :

private readonly WeakEventSource<MyEventArgs> _myEventSource = new WeakEventSource<MyEventArgs>();
public event EventHandler<MyEventArgs> MyEvent
{
    add { _myEventSource.Subscribe(value); }
    remove { _myEventSource.Unsubscribe(value); }
}

Du point de vue de celui qui s’abonne à l’évènement, c’est exactement pareil qu’un évènement normal, mais l’abonné restera éligible à la garbage collection s’il n’est plus référencé nulle part ailleurs.

L’objet qui publie l’évènement peut le déclencher comme ceci :

_myEventSource.Raise(this, e);

Il y a une petite limitation : la signature de l’évènement doit être EventHandler<TEventArgs> (avec ce que vous voulez comme TEventArgs, bien sûr). Ca ne peut pas être quelque chose comme FooEventHandler, ou un type de délégué custom. Je ne pense pas que ce soit un problème majeur, dans la mesure où une vaste majorité des évènements dans le monde .NET respecte le pattern recommandé void (sender, args), et les delegates spécifiques comme FooEventHandler ont en fait la même signature que EventHandler<FooEventArgs>. J’avais d’abord essayé de supporter n’importe quel type de delegate, mais ça s’est avéré un peu trop compliqué… pour l’instant en tout cas Winking smile.

 

Comment ça marche?

La nouvelle solution est encore basée sur des références faibles, mais change la façon dont la méthode cible est appelée. Au lieu d’utiliser DynamicInvoke, on crée un “open-instance delegate” pour la méthode lors de l’abonnement. Cela signifie que pour une méthode ayant une signature comme void EventHandler(object sender, EventArgs e), on  crée un delegate avec la signature void OpenEventHandler(object target, object sender, EventArgs e). Le paramètre supplémentaire target représente l’instance sur laquelle la méthode est appelée. Pour invoquer le gestionnaire de l’évènement, il suffit de récupérer la cible à partir de la référence faible, et si elle est toujours vivante, de la passer au “open-instance delegate”.

Pour de meilleures performances, ce delegate est en fait créé seulement la première fois qu’on rencontre une méthode donnée, et est mis en cache pour être réutilisé ultérieurement. Ainsi, si plusieurs instances d’une classe s’abonnent à l’évènement avec la même méthode, le delegate ne sera créé que la première fois, et sera réutilisé pour les abonnés suivants.

Notez que techniquement, le delegate créé n’est pas un “vrai” open-instance delegate comme ceux créés par la méthode Delegate.CreateDelegate. Il est en fait créé à l’aide des expressions Linq. La raison est que dans un vrai open-instance delegate, le type du premier paramètre doit être le type qui déclare la méthode, et non object. Puisque cette information n’est pas disponible statiquement, il faut introduire un cast dynamiquement.

 

Le code source est disponible sur GitHub: WeakEvent. Un package NuGet est disponible ici : ThomasLevesque.WeakEvent.

Le dépôt GitHub contient aussi des snippets pour Visual Studio et pour ReSharper, pour faciliter l’écriture du code de plomberie pour un weak event.

[C#] Une implémentation du pattern WeakEvent

Comme vous le savez peut-être, la mauvaise utilisation des évènements est l’une des principales causes de fuites mémoires dans une application .NET : en effet, un évènement garde des références aux objets qui y sont abonnés (via le delegate), ce qui empêche le garbage collector de collecter ces objets quand ils ne sont plus utilisés. Le problème est particulièrement vrai pour un évènement statique, puisque les références sont conservées pendant toute l’exécution de l’application. Si on crée de nombreux objets qui s’abonnent à un évènement statique et qu’on ne les désabonne pas, ils restent indéfiniment en mémoire, même si on n’en a plus besoin depuis longtemps, ce qui peut finir par saturer la mémoire.

La solution “évidente” au problème est bien sûr de désabonner les objets qui ne sont plus utilisés. Malheureusement, il n’y a pas toujours de moyen simple de savoir à quel moment on peut désabonner un objet. Une autre approche est d’implémenter le pattern WeakEvent, qui permet de ne garder qu’une référence faible vers les objets abonnés à l’évènement, de façon à ne pas empêcher le garbage collector de les collecter. Microsoft inclut dans WPF des éléments pour implémenter le pattern WeakEvent, et explique comment créer ses propres évènements selon ce pattern, à l’aide de la classe WeakEventManager et de l’interface IWeakEventListener. Cependant, cette technique est assez lourde à mettre en œuvre, aussi bien pour exposer un tel évènement (il faut créer une nouvelle classe dédiée) que pour s’abonner à l’évènement (implémentation de IWeakEventListener).

J’ai donc réfléchi à une autre solution, permettant d’implémenter plus facilement le pattern WeakEvent. Ma première idée était d’utiliser une liste de WeakReference pour stocker la liste des delegates abonnés à l’évènement. Malheureusement, lorsqu’on s’abonne à un évènement, on écrit généralement quelque chose comme ça :

myObject.MyEvent += new EventHandler(myObject_MyEvent);

On crée donc un delegate, mais on ne garde aucune référence dessus. Puisque l’évènement ne référence ce delegate que via une WeakReference, rien n’empêche le garbage collector de le collecter… et c’est effectivement ce qui arrive. Au bout d’un temps variable (pas plus de quelques secondes d’après mes observations), le delegate est collecté et n’est donc plus appelé quand l’évènement est déclenché.

Plutôt que de conserver une référence faible vers le delegate lui même, une meilleure solution serait de faire une référence faible sur l’objet qui implémente la méthode (Delegate.Target). J’ai donc créé une classe WeakDelegate<TDelegate> pour gérer cela :

    public class WeakDelegate<TDelegate> : IEquatable<TDelegate>
    {
        private WeakReference _targetReference;
        private MethodInfo _method;

        public WeakDelegate(Delegate realDelegate)
        {
            if (realDelegate.Target != null)
                _targetReference = new WeakReference(realDelegate.Target);
            else
                _targetReference = null;
            _method = realDelegate.Method;
        }

        public TDelegate GetDelegate()
        {
            return (TDelegate)(object)GetDelegateInternal();
        }

        private Delegate GetDelegateInternal()
        {
            if (_targetReference != null)
            {
                return Delegate.CreateDelegate(typeof(TDelegate), _targetReference.Target, _method);
            }
            else
            {
                return Delegate.CreateDelegate(typeof(TDelegate), _method);
            }
        }

        public bool IsAlive
        {
            get { return _targetReference == null || _targetReference.IsAlive; }
        }


        #region IEquatable<TDelegate> Members

        public bool Equals(TDelegate other)
        {
            Delegate d = (Delegate)(object)other;
            return d != null
                && d.Target == _targetReference.Target
                && d.Method.Equals(_method);
        }

        #endregion

        internal void Invoke(params object[] args)
        {
            Delegate handler = (Delegate)(object)GetDelegateInternal();
            handler.DynamicInvoke(args);
        }
    }

Il ne reste plus qu’à gérer une liste de WeakDelegate<TDelegate>, ce que fait la classe WeakEvent<TDelegate> :

    public class WeakEvent<TEventHandler>
    {
        private List<WeakDelegate<TEventHandler>> _handlers;

        public WeakEvent()
        {
            _handlers = new List<WeakDelegate<TEventHandler>>();
        }

        public virtual void AddHandler(TEventHandler handler)
        {
            Delegate d = (Delegate)(object)handler;
            _handlers.Add(new WeakDelegate<TEventHandler>(d));
        }

        public virtual void RemoveHandler(TEventHandler handler)
        {
            // also remove "dead" (garbage collected) handlers
            _handlers.RemoveAll(wd => !wd.IsAlive || wd.Equals(handler));
        }

        public virtual void Raise(object sender, EventArgs e)
        {
            var handlers = _handlers.ToArray();
            foreach (var weakDelegate in handlers)
            {
                if (weakDelegate.IsAlive)
                {
                    weakDelegate.Invoke(sender, e);
                }
                else
                {
                    _handlers.Remove(weakDelegate);
                }
            }
        }

        protected List<WeakDelegate<TEventHandler>> Handlers
        {
            get { return _handlers; }
        }
    }

Cette classe gère automatiquement la suppression des handlers “morts” (collectés), et fournit une méthode Raise pour faciliter le déclenchement de l’évènement. Elle peut s’utiliser de la façon suivante :

        private WeakEvent<EventHandler> _myEvent = new WeakEvent<EventHandler>();
        public event EventHandler MyEvent
        {
            add { _myEvent.AddHandler(value); }
            remove { _myEvent.RemoveHandler(value); }
        }

        protected virtual void OnMyEvent()
        {
            _myEvent.Raise(this, EventArgs.Empty);
        }

C’est un peu plus long à écrire qu’un évènement “classique”, mais ce n’est finalement pas grand chose par rapport aux avantages que ça apporte… D’ailleurs, on peut facilement créer un “code snippet” pour Visual Studio, qui permet de créer un “évènement faible” en un rien de temps, avec seulement 3 informations à renseigner :

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
  <CodeSnippet Format="1.0.0">
    <Header>
      <Title>wevt</Title>
      <Shortcut>wevt</Shortcut>
      <Description>Code snippet for a weak event</Description>
      <Author>Thomas Levesque</Author>
      <SnippetTypes>
        <SnippetType>Expansion</SnippetType>
      </SnippetTypes>
    </Header>
    <Snippet>
      <Declarations>
        <Literal>
          <ID>type</ID>
          <ToolTip>Event type</ToolTip>
          <Default>EventHandler</Default>
        </Literal>
        <Literal>
          <ID>event</ID>
          <ToolTip>Event name</ToolTip>
          <Default>MyEvent</Default>
        </Literal>
        <Literal>
          <ID>field</ID>
          <ToolTip>Name of the field holding the registered handlers</ToolTip>
          <Default>_myEvent</Default>
        </Literal>
      </Declarations>
      <Code Language="csharp">
        <![CDATA[private WeakEvent<$type$> $field$ = new WeakEvent<EventHandler>();
        public event $type$ $event$
        {
            add { $field$.AddHandler(value); }
            remove { $field$.RemoveHandler(value); }
        }

        protected virtual void On$event$()
        {
            $field$.Raise(this, EventArgs.Empty);
        }
	$end$]]>
      </Code>
    </Snippet>
  </CodeSnippet>
</CodeSnippets>

Ce qui donne dans Visual Studio le résultat suivant :

Code snippet pour implémenter un WeakEvent