web-dev-qa-db-fra.com

Un modèle de comptage de référence pour les langues gérées en mémoire?

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 uniquement 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.

En C++ moderne, vous résoudriez ce problème avec un shared_ptr, ce qui libérerait de manière déterministe la ressource lorsque tous les shared_ptr 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?

11
C. Ross

En général, vous l'évitez en ayant un seul propriétaire, même dans des 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 une Close() vous décrémentez un compteur (incrémenté sur Open()/Connect()/etc) jusqu'à ce que vous atteigniez 0 à quel point la fermeture fait en fait la fermeture. Il ressemblera et agira probablement comme le modèle Flyweight.

15
Telastyn

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 + des gestionnaires de contexte (Python) est un moyen pour flux de contrôle (! = objets) pour contenir une ressource qui est fermée lorsque le flux de contrôle quitte une étendue. Dans tous ces cas, cela ressemble à une try { ... } finally { resource.close(); } insérée automatiquement. 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 survienne à 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é.

14
amon

Beaucoup de bonnes informations des autres réponses.

Néanmoins, pour être explicite, le modèle que vous recherchez peut-être est que vous utilisez de petits objets appartenant individuellement à la construction de flux de contrôle de type RAII via using et IDispose, en conjonction avec un ( objet plus grand, éventuellement compté par référence) qui contient certaines ressources (système d'exploitation).

Il y a donc les petits objets à propriétaire unique non partagés qui (via la construction IDispose et using du flux de contrôle du plus petit objet) peuvent à leur tour informer le plus grand objet partagé (peut-être Acquire & personnalisé Release méthodes).

(Les méthodes Acquire et Release illustrées ci-dessous sont alors également disponibles en dehors de la construction using, mais sans la sécurité du try implicite dans using.)


Un exemple en C #

void Test ( MyRefCountedClass myObj )
{
    using ( var usingRef = myObj.Acquire () )
    {
        var item = usingRef.Item;
        item.SomeMethod ();

        // the `using` automatically invokes Dispose() on usingRef
        //  which in turn invokes Release() on `myObj.
    }
}

interface IReferencable<T> where T: IReferencable<T> {
    Reference<T> Acquire ();
    void Release();
}

struct Reference<T>: IDisposable where T: IReferencable<T>
{
    public readonly T Item;
    public Reference(T item) { Item = item; _released = false; }
    public void Dispose() { if (! _released ) { _released = true; Item.Release(); } }
    private bool _released;
}

class MyRefCountedClass : IReferencable<MyRefCountedClass>
{
    private int _refCount = 0;

    public Reference<MyRefCountedClass> Acquire ()
    {
        _refCount++;
        return new Reference<MyRefCountedClass>(this);
    }

    public void Release ()
    {
        if (--_refCount <= 0)
            Dispose();
    }

    // NOTE that MyRefCountedClass does not have to implement IDisposable, but it can...
    // as shown here it doesn't implement the interface
    private void Dispose ()  
    {
        if ( _refCount > 0 )
            throw new Exception ("Dispose attempted on item in use.");
        // release other resources...
    }

    public int SomeMethod()
    {
        return 0;
    }
}
3
Erik Eidt

La grande majorité des objets d'un système doivent généralement correspondre à l'un des trois modèles suivants:

  1. 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.

  2. Les objets qui sont sous le contrôle exclusif d'une seule entité, qui est le seul 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.

  3. 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 existe 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.

1
supercat

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 y en avait pratiquement aucun car sinon cela impliquerait que l'utilisateur n'a pas besoin de simplement supprimer quelque chose temps d'un endroit, mais supprimez-le explicitement de tous les propriétaires concernés avant que la ressource ne soit réellement supprimée 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 actif 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 du 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 d'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 bien que cela nous ait vraiment aidés à nous assurer que les ressources ne soient jamais 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éé des logiciels qui fuyaient. 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 de manière cohérente 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'a donné le meilleur que j'ai trouvé dans les deux mondes, a été d'éliminer le concept que referencing=ownership mais ont encore reporté 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:

ecs->remove(component);

... 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 " d'arrêter le monde " et de détruire toutes les ressources qui devaient être supprimés lors du verrouillage des threads du traitement de 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 le GC ou le comptage de références et ne risque pas que ces types de fuites de ressources passent 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 de l'utilisateur, et là, il est en fait logique que chaque copie immuable partage potentiellement la propriété des mêmes données non modifiées.

0
user204677