Java et .NET ont de merveilleux récupérateurs qui gèrent la mémoire pour vous et des modèles pratiques pour libérer rapidement des objets externes ( Closeable
, IDisposable
), mais seulement s'ils appartiennent à un seul objet. Dans certains systèmes, une ressource peut avoir besoin d'être consommée indépendamment par deux composants et d'être libérée uniquement lorsque les deux composants libèrent la ressource.
Dans le C ++ moderne, vous résoudriez ce problème avec a shared_ptr
, qui libérerait de manière déterministe la ressource lorsque tous les shared_ptr
fichiers sont détruits.
Existe-t-il des modèles documentés et éprouvés de gestion et de libération de ressources coûteuses qui n'ont pas de propriétaire unique dans des systèmes de collecte des ordures orientés objet et non déterministes?
Réponses:
En général, vous l'évitez en ayant un seul propriétaire, même dans les langues non gérées.
Mais le principe est le même pour les langages gérés. Au lieu de fermer immédiatement la ressource coûteuse sur un,
Close()
vous décrémentez un compteur (incrémenté surOpen()
/Connect()
/ etc) jusqu'à ce que vous atteigniez 0, moment auquel la fermeture fait la fermeture. Il ressemblera et agira probablement comme le modèle Flyweight.la source
IDisposable
, maintenir compte de libérer la ressource le plus tôt possible, etc. Probablement la meilleure chose, beaucoup de temps, est d'avoir tous les trois, mais le finaliseur est probablement la partie la plus critique et laIDisposable
mise en œuvre est le moins critique.Dans un langage garbage collection (où GC n'est pas déterministe), il n'est pas possible de lier de manière fiable le nettoyage d'une ressource autre que la mémoire à la durée de vie d'un objet: il n'est pas possible d'indiquer quand un objet sera supprimé. La fin de vie est entièrement à la discrétion du ramasse-miettes. Le GC garantit seulement qu'un objet vivra tant qu'il sera accessible. Une fois qu'un objet devient inaccessible, il peut être nettoyé à un moment donné dans le futur, ce qui peut impliquer l'exécution de finaliseurs.
Le concept de «propriété des ressources» ne s'applique pas vraiment dans un langage GC. Le système GC possède tous les objets.
Ce que ces langages offrent avec try-with-resource + Closeable (Java), en utilisant des instructions + IDisposable (C #), ou avec des instructions + context managers (Python) est un moyen pour le flux de contrôle (! = Objets) de contenir une ressource qui est fermé lorsque le flux de contrôle quitte une étendue. Dans tous ces cas, cela ressemble à une insertion automatique
try { ... } finally { resource.close(); }
. La durée de vie de l'objet représentant la ressource n'est pas liée à la durée de vie de la ressource: l'objet peut continuer à vivre après la fermeture de la ressource et l'objet peut devenir inaccessible tant que la ressource est toujours ouverte.Dans le cas des variables locales, ces approches sont équivalentes à RAII, mais doivent être utilisées explicitement sur le site d'appel (contrairement aux destructeurs C ++ qui s'exécuteront par défaut). Un bon IDE avertira lorsque cela est omis.
Cela ne fonctionne pas pour les objets référencés à partir d'emplacements autres que des variables locales. Ici, peu importe qu'il y ait une ou plusieurs références. Il est possible de traduire le référencement des ressources via des références d'objet à la propriété des ressources via le flux de contrôle en créant un thread distinct qui contient cette ressource, mais les threads sont également des ressources qui doivent être supprimées manuellement.
Dans certains cas, il est possible de déléguer la propriété des ressources à une fonction appelante. Au lieu que les objets temporaires référencent des ressources qu'ils doivent (mais ne peuvent pas) nettoyer de manière fiable, la fonction appelante contient un ensemble de ressources qui doivent être nettoyées. Cela ne fonctionne que jusqu'à ce que la durée de vie de l'un de ces objets dépasse la durée de vie de la fonction, et fait donc référence à une ressource qui a déjà été fermée. Cela ne peut pas être détecté par un compilateur, à moins que le langage n'ait un suivi de propriété semblable à Rust (auquel cas il existe déjà de meilleures solutions pour ce problème de gestion des ressources).
Cela reste la seule solution viable: la gestion manuelle des ressources, éventuellement en implémentant vous-même le comptage des références. Ceci est sujet aux erreurs, mais pas impossible. En particulier, avoir à penser à la propriété est inhabituel dans les langages du GC, donc le code existant peut ne pas être suffisamment explicite sur les garanties de propriété.
la source
Beaucoup de bonnes informations des autres réponses.
Pourtant, pour être explicite, le modèle que vous recherchez peut-être est que vous utilisez de petits objets appartenant à un seul pour la construction de flux de contrôle de type RAII via
using
etIDispose
, en conjonction avec un objet (plus grand, éventuellement compté par référence) qui en contient (fonctionnant système).Il y a donc les petits objets à propriétaire unique non partagés qui (via les objets plus petits
IDispose
et lausing
construction de flux de contrôle) peuvent à leur tour informer le plus grand objet partagé (peut-être des méthodesAcquire
et desRelease
méthodes personnalisées ).(Les méthodes
Acquire
etRelease
indiquées ci-dessous sont également disponibles en dehors de la construction using, mais sans la sécurité de l'try
implicite dansusing
.)Un exemple en C #
la source
IDisposable.Dispose
stipule que l'appelDispose
plusieurs fois sur le même objet doit être un no-op. Si je devais implémenter un tel modèle, je le rendrais égalementRelease
privé pour éviter les erreurs inutiles et utiliserais la délégation au lieu de l'héritage (supprimez l'interface, fournissez uneSharedDisposable
classe simple qui peut être utilisée avec des jetables arbitraires), mais ce sont plus des questions de goût.La grande majorité des objets d'un système doivent généralement correspondre à l'un des trois modèles suivants:
Objets dont l'état ne changera jamais et auxquels les références sont tenues uniquement comme un moyen d'encapsuler l'état. Les entités qui détiennent des références ne savent ni ne se soucient de savoir si d'autres entités détiennent des références au même objet.
Les objets qui sont sous le contrôle exclusif d'une seule entité, qui est l'unique propriétaire de tous les états qui s'y trouvent, et qui utilisent l'objet uniquement comme moyen d'encapsuler l'état (éventuellement mutable) qui s'y trouve.
Objets appartenant à une seule entité, mais que d'autres entités sont autorisées à utiliser de manière limitée. Le propriétaire de l'objet peut l'utiliser non seulement comme moyen d'encapsuler l'état, mais également encapsuler une relation avec les autres entités qui le partagent.
Le suivi de la récupération de place fonctionne mieux que le comptage de références pour # 1, car le code qui utilise de tels objets n'a pas besoin de faire quoi que ce soit de spécial lorsqu'il est fait avec la dernière référence restante. Le comptage de références n'est pas nécessaire pour # 2 car les objets auront exactement un propriétaire, et il saura quand il n'a plus besoin de l'objet. Le scénario n ° 3 peut poser quelques difficultés si le propriétaire d'un objet le tue alors que d'autres entités détiennent encore des références; même là, un GC de suivi peut être meilleur que le comptage de références pour garantir que les références à des objets morts restent identifiables de manière fiable en tant que références à des objets morts, aussi longtemps que de telles références existent.
Il y a quelques situations où il peut être nécessaire qu'un objet partageable sans propriétaire acquière et détienne des ressources externes tant que quiconque a besoin de ses services, et devrait les libérer lorsque ses services ne sont plus nécessaires. Par exemple, un objet qui encapsule le contenu d'un fichier en lecture seule pourrait être partagé et utilisé par de nombreuses entités simultanément sans qu'aucune d'entre elles n'ait à connaître ou à se soucier de l'existence de l'autre. De telles circonstances sont cependant rares. La plupart des objets auront soit un seul propriétaire clair, soit seront sans propriétaire. La propriété multiple est possible, mais rarement utile.
la source
La propriété partagée a rarement un sens
Cette réponse peut être légèrement décalée, mais je dois demander, combien de cas est-il sensé du point de vue de l'utilisateur de partager la propriété ? Au moins dans les domaines dans lesquels j'ai travaillé, il n'y en avait pratiquement aucun car sinon cela impliquerait que l'utilisateur n'a pas besoin de simplement supprimer quelque chose une fois d'un endroit, mais de le supprimer explicitement de tous les propriétaires concernés avant que la ressource ne soit réellement supprimé du système.
C'est souvent une idée d'ingénierie de niveau inférieur pour empêcher la destruction des ressources pendant que quelque chose d'autre y accède, comme un autre thread. Souvent, lorsqu'un utilisateur demande à fermer / supprimer / supprimer quelque chose du logiciel, il doit être supprimé dès que possible (chaque fois qu'il est sûr de le supprimer), et il ne doit certainement pas s'attarder et provoquer une fuite de ressources aussi longtemps que l'application est en cours d'exécution.
Par exemple, un élément de jeu dans un jeu vidéo peut référencer un matériau de la bibliothèque de matériaux. Nous ne voulons certainement pas, disons, un crash de pointeur pendant si le matériau est supprimé de la bibliothèque de matériaux dans un thread alors qu'un autre thread accède toujours au matériel référencé par l'actif du jeu. Mais cela ne signifie pas qu'il soit logique que les actifs du jeu partagent la propriété des matériaux qu'ils référencent avec la bibliothèque de matériaux. Nous ne voulons pas forcer l'utilisateur à supprimer explicitement le matériau de la bibliothèque de ressources et de matériaux. Nous voulons juste nous assurer que les matériaux ne sont pas supprimés de la bibliothèque de matériel, le seul propriétaire sensé des matériaux, jusqu'à ce que les autres threads aient fini d'accéder au matériel.
Fuites de ressources
Pourtant, j'ai travaillé avec une ancienne équipe qui a adopté GC pour tous les composants du logiciel. Et même si cela a vraiment aidé à nous assurer que nous n'avions jamais de ressources détruites pendant que d'autres threads y accédaient, nous avons finalement obtenu notre part de fuites de ressources .
Et il ne s'agissait pas de fuites de ressources insignifiantes qui ne dérangent que les développeurs, comme un kilo-octet de mémoire qui a fui après une session d'une heure. Il s'agissait de fuites épiques, souvent des gigaoctets de mémoire sur une session active, conduisant à des rapports de bogues. Parce que maintenant, lorsque la propriété d'une ressource est référencée (et donc partagée en propriété) entre, disons, 8 parties différentes du système, il suffit d'une seule pour ne pas supprimer la ressource en réponse à l'utilisateur qui demande qu'elle soit supprimée pour elle à fuir et éventuellement indéfiniment.
Je n'ai donc jamais été un grand fan du GC ou du comptage de références appliqué à grande échelle en raison de la facilité avec laquelle ils ont créé un logiciel qui fuit. Ce qui aurait été autrefois un crash de pointeur pendant qui est facile à détecter se transforme en une fuite de ressources très difficile à détecter qui peut facilement voler sous le radar des tests.
Des références faibles peuvent atténuer ce problème si la langue / bibliothèque les fournit, mais j'ai trouvé difficile d'obtenir une équipe de développeurs de compétences mixtes pour pouvoir utiliser systématiquement des références faibles chaque fois que cela était approprié. Et cette difficulté n'était pas uniquement liée à l'équipe interne, mais à chaque développeur de plugin unique pour notre logiciel. Eux aussi pourraient facilement provoquer une fuite de ressources du système en stockant simplement une référence persistante à un objet de manière à ce qu'il soit difficile de remonter au plugin en tant que coupable, nous avons donc également obtenu notre part du lion des rapports de bogues résultant de nos ressources logicielles être divulgué simplement parce qu'un plugin dont le code source était hors de notre contrôle n'a pas réussi à libérer des références à ces ressources coûteuses.
Solution: suppression différée et périodique
Donc, ma solution plus tard, que j'ai appliquée à mes projets personnels qui m'ont donné le meilleur que j'ai trouvé dans les deux mondes, a été d'éliminer le concept,
referencing=ownership
mais qui a encore retardé la destruction des ressources.Par conséquent, chaque fois que l'utilisateur fait quelque chose qui nécessite la suppression d'une ressource, l'API est exprimée en termes de suppression de la ressource:
... qui modélise la logique utilisateur de manière très simple. Cependant, la ressource (composant) ne peut pas être supprimée immédiatement s'il existe d'autres threads système dans leur phase de traitement où ils pourraient accéder simultanément au même composant.
Donc, ces threads de traitement donnent ensuite du temps ici et là, ce qui permet à un thread qui ressemble à un garbage collector de se réveiller et de " stopper le monde " et de détruire toutes les ressources dont la suppression a été demandée tout en empêchant les threads de traiter ces composants jusqu'à ce qu'il soit terminé . J'ai réglé cela pour que la quantité de travail à faire ici soit généralement minime et ne coupe pas sensiblement les fréquences d'images.
Maintenant, je ne peux pas dire que c'est une méthode éprouvée et bien documentée, mais c'est quelque chose que j'utilise depuis quelques années sans aucun mal de tête et sans fuite de ressources. Je recommande d'explorer des approches comme celle-ci lorsqu'il est possible pour votre architecture de s'adapter à ce type de modèle de concurrence, car il est beaucoup moins lourd que GC ou le comptage de références et ne risque pas ces types de fuites de ressources volant sous le radar des tests.
Le seul endroit où j'ai trouvé le comptage de références ou GC utile est pour les structures de données persistantes. Dans ce cas, c'est le territoire de la structure de données, loin des préoccupations des utilisateurs, et là, il est en fait logique que chaque copie immuable partage potentiellement la propriété des mêmes données non modifiées.
la source