191 votes

Pourquoi et comment éviter les fuites de mémoire des gestionnaires d'événements ?

Je viens de me rendre compte, en lisant certaines questions et réponses sur StackOverflow, que l'ajout de gestionnaires d'événements à l'aide de l'option += en C# (ou, je suppose, dans d'autres langages .net) peut provoquer des fuites de mémoire courantes...

J'ai souvent utilisé des gestionnaires d'événements de ce type dans le passé, sans me rendre compte qu'ils pouvaient provoquer, ou ont provoqué, des fuites de mémoire dans mes applications.

Comment cela fonctionne-t-il (c'est-à-dire, pourquoi cela provoque-t-il une fuite de mémoire) ?
Comment puis-je résoudre ce problème ? Est-ce que l'utilisation de -= au même gestionnaire d'événement ?
Existe-t-il des modèles de conception communs ou des meilleures pratiques pour gérer des situations de ce type ?
Exemple : Comment suis-je censé gérer une application qui comporte de nombreux threads différents, utilisant de nombreux gestionnaires d'événements différents pour déclencher plusieurs événements sur l'interface utilisateur ?

Existe-t-il des moyens simples et efficaces de contrôler ce processus dans une grande application déjà construite ?

224voto

Jon Skeet Points 692016

La cause est simple à expliquer : lorsqu'un gestionnaire d'événements est souscrit, le fichier éditeur de l'événement contient une référence à la abonné via le délégué du gestionnaire d'événement (en supposant que le délégué est une méthode d'instance).

Si l'éditeur vit plus longtemps que l'abonné, il maintiendra l'abonné en vie même s'il n'y a pas d'autres références à l'abonné.

Si vous vous désinscrivez de l'événement avec un gestionnaire égal, alors oui, cela supprimera le gestionnaire et la fuite possible. Cependant, d'après mon expérience, c'est rarement un problème - parce que je trouve généralement que l'éditeur et l'abonné ont des durées de vie à peu près égales de toute façon.

Il est une cause possible... mais d'après mon expérience, c'est plutôt surfait. Votre kilométrage peut varier, bien sûr... vous devez juste être prudent.

0 votes

...J'ai vu certaines personnes écrire à ce sujet dans des réponses à des questions comme "quelles sont les fuites de mémoire les plus courantes en .net".

40 votes

Une façon de contourner ce problème du côté de l'éditeur est de définir l'événement comme nul une fois que vous êtes sûr de ne plus le déclencher. Cela supprimera implicitement tous les abonnés et peut être utile lorsque certains événements ne sont déclenchés qu'à certaines étapes de la vie de l'objet.

3 votes

La méthode Dipose serait un bon moment pour mettre l'événement à zéro.

61voto

Emran Hussain Points 1196

J'ai expliqué cette confusion dans un blog à https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16 . Je vais essayer de le résumer ici pour que vous puissiez vous faire une idée claire.

Référence signifie, "Besoin" :

Tout d'abord, vous devez comprendre que, si l'objet A détient une référence à l'objet B, cela signifie que l'objet A a besoin de l'objet B pour fonctionner, n'est-ce pas ? Donc, le ramasseur de déchets ne collectera pas l'objet B tant que l'objet A est vivant dans la mémoire.

+= Moyens, en injectant la référence de l'objet de droite à l'objet de gauche :

La confusion vient de l'opérateur C# +=. Cet opérateur n'indique pas clairement au développeur que le côté droit de cet opérateur injecte en fait une référence à l'objet du côté gauche.

enter image description here

Et en faisant cela, l'objet A pense qu'il a besoin de l'objet B, même si, de votre point de vue, l'objet A ne devrait pas se soucier de savoir si l'objet B vit ou non. Comme l'objet A pense que l'objet B est nécessaire, l'objet A protège l'objet B du ramasseur de déchets tant que l'objet A est en vie. Mais, si vous ne vouliez pas de cette protection donné à l'objet abonné à l'événement, alors, on peut dire qu'une fuite de mémoire s'est produite. Pour souligner cette affirmation, laissez-moi préciser que, dans le monde .NET, il n'y a pas de concept de fuite de mémoire comme dans un programme typique C++ non géré. Mais, comme je l'ai dit, l'objet A protège l'objet B du ramassage des ordures et si ce n'était pas votre intention, alors vous pouvez dire qu'une fuite de mémoire s'est produite parce que l'objet B n'était pas censé vivre dans la mémoire.

enter image description here

Vous pouvez éviter une telle fuite en détachant le gestionnaire d'événement.

Comment prendre une décision ?

Il y a beaucoup d'événements et de gestionnaires d'événements dans l'ensemble de votre code-base. Cela signifie-t-il que vous devez continuer à détacher des gestionnaires d'événements partout ? La réponse est non. Si vous deviez le faire, votre base de code serait vraiment laide et verbeuse.

Vous pouvez plutôt suivre un organigramme simple pour déterminer si un gestionnaire d'événement de détachement est nécessaire ou non.

enter image description here

La plupart du temps, vous constaterez que l'objet abonné aux événements est aussi important que l'objet éditeur d'événements et que les deux sont censés vivre en même temps.

Exemple d'un scénario où vous ne devez pas vous inquiéter

Par exemple, un événement de clic de bouton d'une fenêtre.

enter image description here

Ici, l'éditeur d'événement est le bouton, et l'abonné d'événement est la fenêtre principale. En appliquant cet organigramme, posez une question : la fenêtre principale (abonné à l'événement) est-elle censée être morte avant le bouton (éditeur d'événement) ? De toute évidence, non. Cela n'a même pas de sens. Alors, pourquoi se préoccuper de détacher le gestionnaire d'événement de clic ?

Un exemple où le détachement d'un gestionnaire d'événement est un MUST.

Je vais vous donner un exemple où l'objet abonné est censé être mort avant l'objet éditeur. Disons que votre MainWindow publie un événement nommé "SomethingHappened" et que vous affichez une fenêtre enfant à partir de la fenêtre principale en cliquant sur un bouton. La fenêtre enfant s'abonne à cet événement de la fenêtre principale.

enter image description here

Et la fenêtre enfant s'abonne à un événement de la fenêtre principale.

enter image description here

A partir de ce code, nous pouvons clairement comprendre qu'il y a un bouton dans la fenêtre principale. En cliquant sur ce bouton, une fenêtre enfant apparaît. La fenêtre enfant écoute un événement de la fenêtre principale. Après avoir fait quelque chose, l'utilisateur ferme la fenêtre enfant.

Maintenant, selon l'organigramme que j'ai fourni, si vous posez la question "Est-ce que la fenêtre enfant (abonné à l'événement) est censée être morte avant l'éditeur d'événement (fenêtre principale) ? La réponse devrait être OUI. N'est-ce pas ? Donc, détachez le gestionnaire d'événement. Je le fais habituellement à partir de l'événement Unloaded de la fenêtre.

Une règle de base : Si votre vue (c'est-à-dire WPF, WinForm, UWP, Xamarin Form, etc.) souscrit à un événement d'un ViewModel, pensez toujours à détacher le gestionnaire d'événement. Car un ViewModel vit généralement plus longtemps qu'une vue. Donc, si le ViewModel n'est pas détruit, toute vue qui a souscrit à un événement de ce ViewModel restera en mémoire, ce qui n'est pas bon.

Preuve du concept en utilisant un profileur de mémoire.

Ce ne sera pas très amusant si nous ne pouvons pas valider le concept avec un profileur de mémoire. J'ai utilisé le profileur de mémoire JetBrain dotMemory dans cette expérience.

D'abord, j'ai lancé la MainWindow, qui s'affiche comme ceci :

enter image description here

Puis, j'ai pris un instantané de la mémoire. Puis j'ai cliqué sur le bouton 3 fois . Trois fenêtres enfants sont apparues. J'ai fermé toutes ces fenêtres enfants et cliqué sur le bouton Force GC dans le profiler dotMemory pour m'assurer que le Garbage Collector est appelé. Ensuite, j'ai pris un autre instantané de la mémoire et je l'ai comparé. Et voilà, notre crainte était fondée. La fenêtre enfant n'était pas collectée par le Garbage Collector même après sa fermeture. Non seulement cela mais le nombre d'objets perdus pour l'objet ChildWindow est également indiqué comme " 3 " (J'ai cliqué 3 fois sur le bouton pour afficher 3 fenêtres enfants).

enter image description here

Ok, alors, j'ai détaché le gestionnaire d'événement comme indiqué ci-dessous.

enter image description here

Ensuite, j'ai effectué les mêmes étapes et vérifié le profileur de mémoire. Cette fois, wow ! plus de fuite de mémoire.

enter image description here

5 votes

Belle illustration.

18voto

Giorgi Points 15760

14voto

Femaref Points 41959

Oui, -= est suffisant, Cependant, il pourrait être assez difficile de garder la trace de chaque événement attribué, jamais. (pour plus de détails, voir le post de Jon). En ce qui concerne le modèle de conception, jetez un coup d'oeil à la page modèle d'événement faible .

1 votes

0 votes

Si je sais qu'un éditeur va vivre plus longtemps que l'abonné, je fais en sorte que l'abonné IDisposable et se désinscrire de l'événement.

3voto

Saurabh Points 11097

Un événement est en fait une liste liée de gestionnaires d'événements.

Lorsque vous faites += new EventHandler sur l'événement, il importe peu que cette fonction particulière ait été ajoutée en tant qu'écouteur auparavant, elle sera ajoutée une fois par +=.

Lorsque l'événement est déclenché, il parcourt la liste liée, élément par élément, et appelle toutes les méthodes (gestionnaires d'événements) ajoutées à cette liste. C'est pourquoi les gestionnaires d'événements sont toujours appelés même lorsque les pages ne sont plus en cours d'exécution, tant qu'elles sont vivantes (enracinées), et elles seront vivantes tant qu'elles seront connectées. Ils seront donc appelés jusqu'à ce que le gestionnaire d'événement soit décroché avec un -= new EventHandler.

Voir ici

y MSDN ICI

0 votes

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X