La gestion non déterministe des ressources est-elle une abstraction qui fuit?


9

D'après ce que je peux voir, il existe deux formes omniprésentes de gestion des ressources: la destruction déterministe et explicite. Des exemples des premiers seraient des destructeurs C ++ et des pointeurs intelligents ou le sous-marin DESTROY de Perl, tandis qu'un exemple du second serait le paradigme des blocs à gérer des ressources de Ruby ou l'interface IDispose de .NET.

Les langues plus récentes semblent opter pour cette dernière, peut-être comme effet secondaire de l'utilisation de systèmes de collecte des ordures de la variété sans comptage de références.

Ma question est la suivante: étant donné que les destructeurs de pointeurs intelligents ou de systèmes de collecte des ordures comptant les références - presque la même chose - permettent la destruction implicite et transparente des ressources, est-ce une abstraction moins fuyante que les types non déterministes qui s'appuient sur des explicites notation?

Je vais donner un exemple concret. Si vous avez trois sous-classes C ++ d'une même superclasse, l'une peut avoir une implémentation qui ne nécessite aucune destruction spécifique. Peut-être que sa magie opère d'une autre manière. Le fait qu'il n'ait pas besoin de destruction spéciale n'est pas pertinent - toutes les sous-classes sont toujours utilisées de la même manière.

Un autre exemple utilise des blocs Ruby. Deux sous-classes doivent libérer des ressources, donc la superclasse opte pour une interface qui utilise un bloc dans le constructeur, même si d'autres sous-classes spécifiques peuvent ne pas en avoir besoin car elles ne nécessitent aucune destruction spéciale.

Est-il vrai que ce dernier divulgue les détails de mise en œuvre de la destruction des ressources, alors que le premier ne le fait pas?

EDIT: Comparer, disons, Ruby à Perl pourrait être plus juste car l'un a une destruction déterministe et l'autre pas, mais ils sont tous deux récupérés.


5
Je suis tenté de dire "oui", mais j'aime entendre ce que les autres ont à dire à ce sujet.
Bart van Ingen Schenau

Destruction transparente des ressources? Mis à part le fait que vous devez utiliser des pointeurs intelligents au lieu de pointeurs normaux? Je ne pense pas que ce soit plus transparent que d'avoir un seul mécanisme (références) pour accéder aux objets (en C ++ vous en avez au moins quatre ou cinq).
Giorgio

@Giorgio: "Les moyens d'accéder à un objet" est assez vague. Voulez-vous dire lire ou écrire? Qualification Const / Volatile? Les pointeurs ne sont pas vraiment "un moyen d'accéder à un objet"; à peu près n'importe quelle expression se traduit par un objet et le déréférencement d'un pointeur n'est tout simplement pas si spécial.
MSalters

1
@Giorgio: Dans ce sens OOP, vous ne pouvez pas envoyer de message à un pointeur C ++. Vous devez déréférencer le pointeur pour envoyer le message (*ptr).Message()ou de manière équivalente ptr->Message(). Il existe un ensemble infini d'expressions autorisées, comme ((*ptr))->Messagec'est également équivalent. Mais ils se résument tous àexpressionIdentifyingAnObject.Message()
MSalters

1
Avec le recomptage, vous devez faire attention à éviter les cercles. De sorte que l'abstraction fuit également, juste d'une manière différente.
CodesInChaos

Réponses:


2

Votre propre exemple répond à la question. La destruction transparente est nettement moins étanche que la destruction explicite. Il peut fuir, mais l'est moins.

La destruction explicite est analogue à malloc / free en C avec tous les pièges. Peut-être avec du sucre syntaxique pour le faire apparaître basé sur la portée.

Certains des avantages de la destruction transparente par rapport à l'explicite: -
même modèle d'utilisation
- vous ne pouvez pas oublier de libérer la ressource.
- les détails de nettoyage ne jonchent pas le paysage au point d'utilisation.


2

L'échec dans l'abstraction n'est en fait pas le fait que la collecte des ordures n'est pas déterministe, mais plutôt dans l'idée que les objets sont "intéressés" par les choses auxquelles ils renvoient et ne sont pas intéressés par les choses auxquelles ils ne tiennent pas. références. Pour voir pourquoi, considérons le scénario d'un objet qui maintient un compteur de la fréquence à laquelle un contrôle particulier est peint. Lors de la création, il souscrit à l'événement "paint" du contrôle, et lors de l'élimination, il se désabonne. L'événement click incrémente simplement un champ et une méthode getTotalClicks()renvoie la valeur de ce champ.

Lorsque l'objet compteur est créé, il doit provoquer le stockage d'une référence à lui-même dans le contrôle qu'il surveille. Le contrôle ne se soucie vraiment pas du contre-objet et serait tout aussi heureux si le contre-objet et la référence à celui-ci cessaient d'exister, mais tant que la référence existe, il appellera le gestionnaire d'événements de cet objet à chaque fois. il se peint. Cette action est totalement inutile au contrôle, mais serait utile à quiconque invoquerait jamais getTotalClicks()l'objet.

Si, par exemple, une méthode devait créer un nouvel objet "compteur de peinture", effectuer une action sur le contrôle, observer combien de fois le contrôle a été repeint, puis abandonner l'objet compteur de peinture, l'objet resterait abonné à l'événement même bien que personne ne s'en soucierait jamais si l'objet et toutes les références à lui disparaissaient simplement. Les objets ne deviendraient cependant pas éligibles à la collecte avant le contrôle lui-même. Si la méthode était invoquée plusieurs milliers de fois au cours de la durée de vie du contrôle [un scénario plausible], elle pourrait provoquer un débordement de mémoire, mais pour le fait que le coût de N invocations serait probablement O (N ^ 2) ou O (N ^ 3) à moins que le traitement des abonnements ne soit très efficace et que la plupart des opérations n'impliquent en fait aucune peinture.

Ce scénario particulier pourrait être géré en donnant au contrôle de conserver une référence faible au contre-objet plutôt qu'une référence forte. Un modèle d'abonnement faible est utile, mais ne fonctionne pas dans le cas général. Supposons qu'au lieu de vouloir avoir un objet qui surveille un seul type d'événement à partir d'un seul contrôle, on voulait avoir un objet enregistreur d'événements qui surveillait plusieurs contrôles, et le mécanisme de gestion des événements du système était tel que chaque contrôle avait besoin d'une référence à un autre objet du journal des événements. Dans ce cas, l'objet reliant un contrôle à l'enregistreur d'événements ne doit rester actif que tant que les deuxle contrôle surveillé et le journal des événements restent utiles. Si ni le contrôle ni le journal des événements ne contiennent une référence forte à l'événement de liaison, il cessera d'exister même s'il est toujours "utile". Si l'un ou l'autre détient un événement fort, la durée de vie de l'objet de liaison peut être inutilement prolongée même si l'autre meurt.

Si aucune référence à un objet n'existe nulle part dans l'univers, l'objet peut en toute sécurité être considéré comme inutile et éliminé de l'existence. Le fait qu'une référence existe à un objet, cependant, n'implique pas que l'objet est "utile". Dans de nombreux cas, l'utilité réelle des objets dépendra de l'existence de références à d' autres objets qui, du point de vue du GC, ne leur sont absolument pas liés.

Si les objets sont notifiés de manière déterministe lorsque personne ne les intéresse, ils pourront utiliser ces informations pour s'assurer que quiconque bénéficierait de ces connaissances en sera informé. En l'absence d'une telle notification, cependant, il n'existe aucun moyen général de déterminer quels objets sont considérés comme "utiles" si l'on ne connaît que l'ensemble des références qui existent, et non le sens sémantique attaché à ces références. Ainsi, tout modèle qui suppose que l'existence ou l'inexistence de références est suffisante pour une gestion automatisée des ressources serait voué à l'échec même si le GC pouvait détecter instantanément l'abandon d'objets.


0

Aucun "destructeur, ou autre interface qui dit" cette classe doit être détruite "n'est un contrat de cette interface. Si vous créez un sous-type qui ne nécessite pas de destruction spéciale, je serais enclin à considérer qu'une violation du principe de substitution de Liskov .

Quant à C ++ par rapport aux autres, il n'y a pas beaucoup de différence. C ++ force cette interface sur tous ses objets. Les abstractions ne peuvent pas fuir lorsqu'elles sont requises par la langue.


4
"Si vous créez un sous-type qui ne nécessite pas de destruction spéciale" Ce n'est pas une violation LSP, car no-op est un cas spécial de destruction valide. Le problème est lorsque vous ajoutez l'exigence de destruction à une classe dérivée.
CodesInChaos

Je m'embrouille ici. Si l'on a besoin d'ajouter du code de destruction spécial à une sous-classe C ++, cela ne change pas du tout ses modèles d'utilisation, car c'est automatique. Cela signifie que la superclasse et la sous-classe peuvent toujours être utilisées de manière interchangeable. Mais avec une notation explicite pour la gestion des ressources, une sous-classe nécessitant une destruction explicite rendrait son utilisation incompatible avec une superclasse, n'est-ce pas? (En supposant que la superclasse n'avait pas besoin d'une destruction explicite.)
Louis Jackman

@CodesInChaos - ah oui, je suppose que c'est vrai.
Telastyn

@ljackman: Une classe qui nécessite une destruction spéciale impose un fardeau à celui qui appelle son constructeur pour s'assurer qu'elle soit exécutée. Cela ne crée pas de violation LSP car un DerivedFooThatRequiresSpecialDestructionne peut être créé que par du code qui appelle new DerivedFooThatRequiresSpecialDestruction(). D'un autre côté, une méthode d'usine qui retourne un DerivedFooThatRequiresSpecialDestructioncode qui ne s'attendait pas à quelque chose nécessitant une destruction serait une violation LSP.
supercat

0

Ma question est la suivante: étant donné que les destructeurs de pointeurs intelligents ou de systèmes de collecte des ordures comptant les références - presque la même chose - permettent la destruction implicite et transparente des ressources, est-ce une abstraction moins fuyante que les types non déterministes qui s'appuient sur des explicites notation?

Devoir surveiller les cycles à la main n'est ni implicite ni transparent. La seule exception est un système de comptage de référence avec un langage qui interdit les cycles par conception. Erlang pourrait être un exemple d'un tel système.

Donc, les deux approches fuient. La principale différence est que les destructeurs fuient partout en C ++ mais IDisposesont très rares sur .NET.


1
Sauf que les cycles sont extrêmement rares et ne se produisent pratiquement jamais, sauf dans les structures de données qui sont explicitement cycliques. La principale différence est que les destructeurs en C ++ sont gérés correctement partout mais IDispose gère rarement le problème dans .NET.
DeadMG

"Sauf que les cycles sont extrêmement rares". Dans les langues modernes? Je contesterais cette hypothèse.
Jon Harrop
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.