Pourquoi la récupération de place ne s’étend qu’à la mémoire et non aux autres types de ressources?

12

Il semble que les gens se soient lassés de la gestion manuelle de la mémoire, alors ils ont inventé la collecte des ordures et la vie était assez bonne. Mais qu'en est-il de tous les autres types de ressources? Des descripteurs de fichiers, des sockets ou même des données créées par l'utilisateur comme les connexions à la base de données?

Cela ressemble à une question naïve mais je ne trouve aucun endroit où quelqu'un l'ait posée. Prenons les descripteurs de fichiers. Supposons qu'un programme sache qu'il ne sera autorisé à disposer de 4000 fds qu'au démarrage. Chaque fois qu'il effectue une opération qui ouvrira un descripteur de fichier, que se passe-t-il s'il

  1. Vérifiez qu'il n'est pas sur le point de s'épuiser.
  2. Si c'est le cas, déclenchez le garbage collector, ce qui libérera un tas de mémoire.
  3. Si une partie de la mémoire libérée contenait des références aux descripteurs de fichiers, fermez-les immédiatement. Il sait que la mémoire appartenait à une ressource car la mémoire liée à cette ressource a été enregistrée dans un «registre de descripteurs de fichiers», faute de meilleur terme, lors de son ouverture.
  4. Ouvrez un nouveau descripteur de fichier, copiez-le dans une nouvelle mémoire, enregistrez cet emplacement de mémoire dans le «registre des descripteurs de fichier» et renvoyez-le à l'utilisateur.

Ainsi, la ressource ne serait pas libérée rapidement, mais elle le serait chaque fois que le gc s'exécutait, ce qui inclut au moins, juste avant que la ressource ne soit sur le point de s'épuiser, en supposant qu'elle ne soit pas entièrement utilisée.

Et il semble que cela serait suffisant pour de nombreux problèmes de nettoyage des ressources définies par l'utilisateur. J'ai réussi à trouver un seul commentaire ici qui fait référence à un nettoyage similaire à celui-ci en C ++ avec un thread qui contient une référence à une ressource et le nettoie lorsqu'il ne reste qu'une seule référence (à partir du thread de nettoyage), mais je peux ' t trouver aucune preuve qu'il s'agit d'une bibliothèque ou d'une partie d'une langue existante.

lecteur d'esprit
la source

Réponses:

4

GC traite d'une ressource prévisible et réservée . La machine virtuelle a un contrôle total sur elle et a un contrôle total sur les instances qui sont créées et quand. Les mots clés ici sont "réservés" et "contrôle total". Les poignées sont allouées par le système d'exploitation et les pointeurs sont ... des pointeurs sur les ressources allouées en dehors de l'espace géré. Pour cette raison, les poignées et les pointeurs ne sont pas limités à être utilisés dans le code managé. Ils peuvent être utilisés - et le sont souvent - par du code managé et non managé exécuté sur le même processus.

Un "Resource Collector" serait en mesure de vérifier si une poignée / un pointeur est utilisé dans un espace géré ou non, mais par définition, il ne sait pas ce qui se passe en dehors de son espace mémoire (et, pour aggraver les choses, certaines poignées peuvent être utilisées au-delà des frontières du processus).

Un exemple pratique est le .NET CLR. On peut utiliser C ++ aromatisé pour écrire du code qui fonctionne avec des espaces mémoire gérés et non gérés; des poignées, des pointeurs et des références peuvent être transmis entre du code managé et non managé. Le code non managé doit utiliser des constructions / types spéciaux pour permettre au CLR de garder la trace des références faites à ses ressources gérées. Mais c'est le mieux qu'il puisse faire. Il ne peut pas faire de même avec les poignées et les pointeurs, et à cause de cela, ledit collecteur de ressources ne saurait pas s'il est autorisé de libérer une poignée ou un pointeur particulier.

edit: Concernant le .NET CLR, je ne suis pas expérimenté avec le développement C ++ avec la plate-forme .NET. Peut-être qu'il existe des mécanismes spéciaux en place qui permettent au CLR de garder la trace des références aux descripteurs / pointeurs entre le code managé et non managé. Si tel est le cas, le CLR pourrait prendre en charge la durée de vie de ces ressources et les libérer lorsque toutes les références à celles-ci seraient effacées (enfin, du moins dans certains scénarios, cela pourrait l'être). Dans les deux cas, les meilleures pratiques dictent que les poignées (en particulier celles pointant vers des fichiers) et les pointeurs doivent être libérés dès qu'ils ne sont pas nécessaires. Un collecteur de ressources ne respecterait pas cela, c'est une autre raison de ne pas en avoir.

edit 2: Il est relativement trivial sur le CLR / JVM / VMs en général d'écrire du code pour libérer un handle particulier s'il est utilisé uniquement à l'intérieur de l'espace géré. Dans .NET serait quelque chose comme:

// This class offends many best practices, but it would do the job.
public class AutoReleaseFileHandle {
    // keeps track of how many instances of this class is in memory
    private static int _toBeReleased = 0;

    // the threshold when a garbage collection should be forced
    private const int MAX_FILES = 100;

    public AutoReleaseFileHandle(FileStream fileStream) {
       // Force garbage collection if max files are reached.
       if (_toBeReleased >= MAX_FILES) {
          GC.Collect();
       }
       // increment counter
       Interlocked.Increment(ref _toBeReleased);
       FileStream = fileStream;
    }

    public FileStream { get; private set; }

    private void ReleaseFileStream(FileStream fs) {
       // decrement counter
       Interlocked.Decrement(ref _toBeReleased);
       FileStream.Close();
       FileStream.Dispose();
       FileStream = null;
    }

    // Close and Dispose the Stream when this class is collected by the GC.
    ~AutoReleaseFileHandle() {
       ReleaseFileStream(FileStream);
    }

    // because it's .NET this class should also implement IDisposable
    // to allow the user to dispose the resources imperatively if s/he wants 
    // to.
    private bool _disposed = false;
    public void Dispose() {
      if (_disposed) {
        return;
      }
      _disposed = true;
      // tells GC to not call the finalizer for this instance.
      GC.SupressFinalizer(this);

      ReleaseFileStream(FileStream);
    }
}

// use it
// for it to work, fs.Dispose() should not be called directly,
var fs = File.Open("path/to/file"); 
var autoRelease = new AutoReleaseFileHandle(fs);
Marcelo De Zen
la source
3

Cela semble être une des raisons pour lesquelles les garbage collector implémentent des finaliseurs. Les finaliseurs sont destinés à permettre à un programmeur de nettoyer les ressources d'un objet pendant la récupération de place. Le gros problème avec les finaliseurs est qu'ils ne sont pas garantis pour fonctionner.

Il y a un très bon compte rendu sur l'utilisation des finaliseurs ici:

Finalisation et nettoyage des objets

En fait, il utilise spécifiquement le descripteur de fichier comme exemple. Vous devez vous assurer de nettoyer ces ressources vous-même, mais il existe un mécanisme qui PEUT restaurer les ressources qui n'ont pas été correctement libérées.

Brian Hibbert
la source
Je ne sais pas si cela répond à ma question. Il manque la partie de ma proposition où le système sait qu'il est sur le point de manquer de ressources. La seule façon de marteler cette partie est de vous assurer que vous exécutez manuellement le gc avant d'allouer de nouveaux descripteurs de fichiers, mais cela est extrêmement inefficace, et je ne sais pas si vous pouvez même faire fonctionner le gc en java.
mindreader
D'accord, mais les descripteurs de fichiers représentent généralement un fichier ouvert dans le système d'exploitation, ce qui implique (selon le système d'exploitation) d'utiliser des ressources de niveau système telles que des verrous, des pools de mémoire tampon, des pools de structure, etc. Franchement, je ne vois pas l'avantage de laisser ces structures ouvertes pour une collecte ultérieure des ordures et je vois de nombreux inconvénients à les laisser allouées plus longtemps que nécessaire. Les méthodes Finalize () sont destinées à permettre un dernier nettoyage de fossé dans le cas où un programmeur aurait ignoré les appels pour nettoyer les ressources, mais ne devrait pas être utilisé.
Brian Hibbert
Je crois comprendre que la raison pour laquelle ils ne devraient pas être invoqués est que si vous allouez une tonne de ces ressources, comme peut-être que vous descendez dans une hiérarchie de fichiers en ouvrant chaque fichier, vous risquez d'ouvrir trop de fichiers avant que le GC arrive à courir, provoquant une explosion. La même chose se produirait avec la mémoire, sauf que le runtime vérifie pour s'assurer qu'il ne manquera pas de mémoire. Je voudrais savoir pourquoi un système ne peut pas être implémenté pour récupérer des ressources arbitraires avant l'explosion, de la même manière que la mémoire.
mindreader
Un système POURRAIT être écrit sur des ressources GC autres que la mémoire, mais vous devrez suivre les comptages de références ou avoir une autre méthode pour déterminer quand une ressource n'est plus utilisée. Vous NE VOULEZ PAS désallouer et réallouer des ressources qui sont encore en cours d'utilisation. Tout manoir de chaos peut s'ensuivre si un thread a un fichier ouvert pour l'écriture, le système d'exploitation "récupère" le handle de fichier et un autre thread ouvre un fichier différent pour l'écriture en utilisant le même handle. Et je suggérerais également que c'est un gaspillage de ressources importantes de les laisser ouvertes jusqu'à ce qu'un thread de type GC se déplace pour les libérer.
Brian Hibbert
3

Il existe de nombreuses techniques de programmation pour aider à gérer ces types de ressources.

  • Les programmeurs C ++ utilisent souvent un modèle appelé Resource Acquisition is Initialization , ou RAII pour faire court. Ce modèle garantit que lorsqu'un objet qui détient des ressources devient hors de portée, il ferme les ressources auxquelles il était attaché. Ceci est utile lorsque la durée de vie de l'objet correspond à une portée particulière dans le programme (par exemple, quand il correspond au moment où un cadre de pile particulier est présent sur la pile), donc il est utile pour les objets qui sont pointés par des variables locales (pointeur variables stockées sur la pile), mais pas très utile pour les objets pointés par des pointeurs stockés sur le tas.

  • Java, C # et de nombreux autres langages fournissent un moyen de spécifier une méthode qui sera invoquée lorsqu'un objet n'est plus actif et sur le point d'être collecté par le garbage collector. Voir, par exemple, les finaliseurs dispose(), et autres. L'idée est que le programmeur peut implémenter une telle méthode afin de fermer explicitement la ressource avant que l'objet ne soit libéré par le garbage collector. Cependant, ces approches ont certains problèmes, que vous pouvez lire ailleurs; par exemple, le garbage collector peut ne pas collecter l'objet bien plus tard que vous le souhaitez.

  • C # et d'autres langages fournissent un usingmot-clé qui aide à garantir que les ressources sont fermées une fois qu'elles ne sont plus nécessaires (donc n'oubliez pas de fermer le descripteur de fichier ou une autre ressource). C'est souvent mieux que de s'appuyer sur le ramasse-miettes pour découvrir que l'objet n'est plus vivant. Voir, par exemple, /programming//q/75401/781723 . Le terme général ici est une ressource gérée . Cette notion s'appuie sur RAII et les finaliseurs, les améliorant à certains égards.

DW
la source
Je suis moins intéressé par une désallocation rapide des ressources et plus intéressé par l'idée d'une désallocation juste à temps. RIAA est génial, mais pas super applicable à de nombreux langages de collecte de déchets. Java n'a pas la capacité de savoir quand il est sur le point de manquer d'une certaine ressource. Les opérations de type et de parenthèse sont utiles et gèrent les erreurs, mais je ne les intéresse pas. Je veux simplement allouer des ressources, puis ils se nettoieront chaque fois que cela sera pratique ou nécessaire, et il n'y a guère de moyen de le gâcher. Je suppose que personne ne s'est vraiment penché sur cette question.
mindreader
2

Toute la mémoire est égale, si je demande 1K, peu m'importe d'où vient l'espace 1K dans l'espace d'adressage.

Lorsque je demande un descripteur de fichier, je veux un descripteur du fichier que je souhaite ouvrir. L'ouverture d'un descripteur de fichier sur un fichier bloque souvent l'accès au fichier par d'autres processus ou machine.

Par conséquent, les descripteurs de fichiers doivent être fermés dès qu'ils ne sont pas nécessaires, sinon ils bloquent les autres accès au fichier, mais la mémoire ne doit être récupérée que lorsque vous commencez à en manquer.

L'exécution d'une passe GC est coûteuse et n'est effectuée que «lorsque cela est nécessaire», il n'est pas possible de prédire quand un autre processus aura besoin d'un descripteur de fichier que votre processus peut ne plus utiliser, mais qu'il a toujours ouvert.

Ian Ringrose
la source
Votre réponse touche la vraie clé: la mémoire est fongible, et la plupart des systèmes en ont assez pour ne pas avoir besoin d'être récupérés particulièrement rapidement. En revanche, si un programme acquiert un accès exclusif à un fichier, cela bloquera tous les autres programmes partout dans l'univers qui pourraient avoir besoin d'utiliser ce fichier, quel que soit le nombre d'autres fichiers pouvant exister.
supercat
0

Je suppose que la raison pour laquelle cela n'a pas été beaucoup abordé pour d'autres ressources est exactement parce que la plupart des autres ressources sont préférables d'être libérées dès que possible pour que quiconque puisse les réutiliser.

Notez bien sûr que votre exemple pourrait maintenant être fourni en utilisant des descripteurs de fichiers "faibles" avec les techniques GC existantes.

Mark Hurd
la source
0

Vérifier si la mémoire n'est plus accessible (et donc garantie de ne plus être utilisée) est assez simple. La plupart des autres types de ressources peuvent être gérées plus ou moins par les mêmes techniques (c'est-à-dire que l'acquisition de ressources est l'initialisation, RAII et son équivalent de libération lorsque l'utilisateur est détruit, ce qui le relie à l'administration de la mémoire). Faire une sorte de libération "juste à temps" est en général impossible (vérifiez le problème d'arrêt, vous devrez découvrir qu'une certaine ressource a été utilisée pour la dernière fois). Oui, parfois cela peut être fait automatiquement, mais c'est un cas beaucoup plus compliqué que la mémoire. Il repose donc principalement sur l'intervention de l'utilisateur.

vonbrand
la source