¿Cómo funciona el conteo de referencias de un puntero inteligente de conteo de referencias?

10 minutos de lectura

En otras palabras, ¿cómo realiza la implementación un seguimiento del conteo?

¿Se mantiene un objeto similar a un mapa al que puedan acceder todos los shared_ptr instancias cuya clave es la dirección del puntero y el valor es el número de referencias? Si tengo que implementar un shared_ptresta es la primera idea que se me ocurre.

¿Existe la posibilidad de una pérdida de memoria en el caso de estos punteros inteligentes de conteo de referencias? Si es así, ¿cómo puedo evitarlos?

avatar de usuario
Ferruccio

He visto dos enfoques no intrusivos diferentes para esto:

  1. El puntero inteligente asigna un pequeño bloque de memoria para contener el contador de referencia. Cada copia del puntero inteligente recibe un puntero al objeto real y un puntero al recuento de referencia.
  2. Además de un puntero de objeto, cada puntero inteligente contiene un puntero anterior y siguiente, formando así una lista doblemente enlazada de punteros inteligentes a un objeto en particular. El recuento de referencias está implícito en la lista. Cuando se copia un puntero inteligente, se agrega a la lista. Tras la destrucción, cada puntero inteligente se elimina de la lista. Si es el último de la lista, también libera el objeto al que se hace referencia.

Si vas aquí y desplácese hasta el final, hay un diagrama excelente que explica estos métodos mucho más claramente.

  • El enfoque de lista enlazada evita la asignación adicional, pero es muy difícil hacer que sea “seguro para subprocesos” sin un mutex global. (“seguro para subprocesos” como en “tan seguro para subprocesos como un puntero sin formato”)

    – chico curioso

    10 de octubre de 2011 a las 14:27

  • También si usas make_sharedtambién puede evitar la asignación adicional colocando el objeto asignado y el contador de instancias en un solo bloque de memoria.

    – Ferruccio

    26 de agosto de 2012 a las 12:33

  • No sabía sobre el enfoque de lista enlazada. Puedo ver algunas ventajas sobre el primer enfoque con el que estoy familiarizado. A saber, la falta del “pequeño bloque de memoria” adicional y la falta de preocupación por el desbordamiento aritmético.

    – Asimilador

    14 de julio de 2016 a las 3:45

  • @Assimilater: no creo que el método de lista vinculada tenga ninguna ventaja real. Tiene que mantener la lista, lo que tiene implicaciones para la seguridad y el rendimiento de los subprocesos. Mientras que la asignación de bloque adicional generalmente desaparece si usa make_shared y todos debería estar usando make_shared de todos modos. Además, si el desbordamiento aritmético es una posibilidad real en este contexto, sospecho que hay problemas mucho más serios con el código base.

    – Ferruccio

    14 de julio de 2016 a las 12:29

  • @Assimilater: asigna un bloque lo suficientemente grande como para contener tanto el objeto como el recuento de referencias. Luego almacena el recuento de referencia justo después del último byte del objeto.

    – Ferruccio

    14 de julio de 2016 a las 16:23

Cada objeto de puntero inteligente contiene un recuento de referencia compartido, uno para cada puntero sin procesar.

Podrías echar un vistazo a este artículo. Esta implementación los almacena en un objeto separado que se copia. También podrías echar un vistazo a documentación de impulso o echar un vistazo a la artículo de wikipedia en punteros inteligentes.

  • -1. Todos los punteros inteligentes que hacen referencia al mismo objeto deben Cuota un solo recuento de referencia. El objeto que contiene el conteo de referencia es asignado por el primer objeto de puntero inteligente, y punteros a ella (NO el objeto en sí) se copian cuando se copia el puntero inteligente.

    – j_random_hacker

    7 de abril de 2009 a las 11:45

  • De acuerdo en j_random_hacker. El conteo es único para cada puntero sin procesar y compartido por todos shared_ptr que se refieren al mismo puntero sin procesar. Por lo general, se asigna como una parte separada de la memoria, por lo que smart_ptr contiene dos ptrs internos, uno para el recuento de referencias y otro para el puntero en sí.

    – David Rodríguez – dribeas

    7 de abril de 2009 a las 13:27

  • -1 para variable estática. A menos que esté implementando un puntero inteligente contado por referencia a un objeto único, no puede usar estática para implementar el conteo de referencia.

    – John Dibling

    7 de abril de 2009 a las 14:17

  • Supongo que arruiné mi redacción, QUISE DECIR que cada puntero tiene un recuento de referencias y que cada puntero inteligente hace referencia a él. Lo arreglé (espero)

    usuario21037

    8 de abril de 2009 a las 8:43

  • Creo que quizás la respuesta de Ferruccio sería una respuesta mejor aceptada.

    usuario21037

    8 de abril de 2009 a las 8:45

Crear una fuga de memoria con punteros inteligentes de conteo de referencias es muy fácil. Simplemente cree cualquier estructura similar a un gráfico de objetos que tenga un ciclo en el gráfico. Los objetos en el ciclo evitarán que se suelten entre sí. Esto no se puede resolver automáticamente; por ejemplo, cuando crea una lista de doble enlace, debe tener cuidado de no eliminar nunca más de un objeto a la vez.

Muchas respuestas abordan la forma en que se almacena el recuento de referencias (se almacena en una memoria compartida para todos los shared_ptr que contienen el mismo puntero nativo), pero la mayoría elude el problema de las fugas.

La forma más fácil de perder memoria con punteros contados de referencia es crear ciclos. Como ejemplo, se garantiza que no se eliminará una lista doblemente enlazada donde todos los punteros son shared_ptr con al menos dos elementos. Incluso si se liberan los punteros externos, los punteros internos seguirán contando y el recuento de referencias no llegará a 0. Eso es, al menos, con la implementación más ingenua.

La solución más sencilla al problema del ciclo es mezclar shared_ptr (punteros de referencia contados) con punteros débiles que no comparten la propiedad del objeto.

Los punteros compartidos compartirán tanto el recurso (puntero) como la información adicional de recuento_referencia. Cuando utiliza punteros débiles, el recuento de referencias se duplica: hay un recuento de referencias de punteros compartidos y un recuento de referencias de punteros débiles. El recurso se libera cada vez que el recuento de punteros compartidos llega a 0, pero la información de reference_count permanece activa hasta que se libera el último puntero débil.

En la lista doblemente enlazada, la referencia externa se mantiene en shared_ptr, mientras que los enlaces internos son solo débil_ptr. Siempre que no haya referencias externas (shared_ptr) se liberan los elementos de la lista, eliminando las referencias débiles. Al final, todas las referencias débiles se han eliminado y el último puntero débil a cada recurso libera la información de recuento de referencias.

Es menos confuso de lo que parece el texto anterior… Lo intentaré de nuevo más tarde.

No. shared_ptr solo mantenga un puntero adicional para el recuento de referencias.

Cuando hace una copia del objeto shared_ptr, copia el puntero con el recuento de referencias, lo aumenta y copia el puntero en el objeto contenido.

avatar de usuario
Cătălin Pitiș

Por lo que recuerdo, estaba el problema del puntero de conteo de referencias tratado en un capítulo de C++ efectivo.

En principio, tiene la clase de puntero “ligera”, que contiene un puntero a una clase que contiene la referencia que sabe incrementar/disminuir la referencia y destruir el objeto del puntero. Esa clase de conteo de referencias apunta al objeto al que se hace referencia.

La clase que implementa RC básicamente lleva la cuenta del número de referencias (de otros objetos de la clase, incluido el propio) a la dirección de memoria que está administrando. La memoria se libera solo cuando el recuento de referencias a la dirección de memoria es cero.

Veamos un poco de código:

template <class T>
class SharedPtr
{
    T* m_ptr;   
    unsigned int* r_count;  
public:
    //Default Constructor
    SharedPtr(T* ptr) :m_ptr{ ptr }, r_count{ ptr ? new unsigned int : nullptr }
    {
        if (r_count)
        {
            *r_count = 1;
        }
    }

    //Copy Constructor
    SharedPtr(SharedPtr& ptr) :m_ptr{ ptr.m_ptr }, r_count{ ptr.m_ptr ? new unsigned int : nullptr }
    {
        if (ptr.r_count)
        {
            ++(*ptr.r_count);
            r_count = ptr.r_count;
            m_ptr = ptr.m_ptr;
        }
    }

    //Copy Assignment
    SharedPtr& operator=(SharedPtr& ptr)
    {
        if (&ptr == this)
            return *this;
        if (ptr.r_count)
        {
            delete m_ptr;
            ++(*ptr.r_count);
            r_count = ptr.r_count;
            m_ptr = ptr.m_ptr;
        }
        return *this;
    }

    //Destructor
    ~SharedPtr()
    {
        if (r_count)
        {
            --(*r_count);
            if (!(*r_count))
            {
                delete m_ptr;
                delete r_count;
            }
        }
    }
};

Aquí está el detalle de cómo el SharedPtr la clase anterior funciona:

Variables internas

El puntero interno m_ptr

Un puntero del SharedPtr class, que es el puntero real utilizado para administrar la memoria en cuestión. Esta variable de puntero se comparte entre varios SharedPtr objetos, por lo que necesitamos un sistema de conteo de referencia para realizar un seguimiento de cuántos SharedPtr los objetos administran la memoria a la que apunta este puntero en cualquier momento durante la vida útil de un programa.

El contador de referencia r_count

Este es un puntero a una variable de tipo entero, que también se comparte entre múltiples SharedPtr objetos que manejan la misma memoria. Esto se comparte porque, cada SharedPtr el objeto que administra la memoria debe ser consciente del recuento de todos los demás SharedPtr objeto que está administrando la misma memoria. La forma de lograr esto es tener un contador de referencia común al que se hace referencia por SharedPtr objetos de la misma familia.

Cada vez que un nuevo SharedPtr objeto se materializa para gestionar una memoria que ya está siendo gestionada por otros SharedPtr objeto/s, el r_count aumenta en 1. También disminuye en 1 cuando un SharedPtr objeto muere, por lo que otro SharedPtr los objetos ‘saben’ que uno de los miembros de su familia que estaba administrando la memoria mantenida por la familia ha muerto y ya no administra la memoria.

Constructor predeterminado

cuando un nuevo SharedPtr El objeto es creado e inicializado por una memoria asignada de almacenamiento dinámico, este constructor se llama donde el puntero interno m_ptr se inicializa en la dirección de memoria asignada del montón que necesita administración. Dado que esta es la primera y única referencia a ese puntero, el contador de referencia r_count se establece en 1. Aquí no sucede nada interesante.

Copiar constructor y copiar asignación

Aquí es donde ocurre el conteo de referencia ‘real’.

Siempre que un nuevo SharedPtr objeto se hace usando otro SharedPtr objeto o un existente SharedPtr se hace referencia a otro SharedPtr es decir, básicamente cuando un nuevo SharedPtr objeto (ya sea existente o recién creado) está hecho para administrar una memoria que ya estaba siendo administrada por otros SharedPtr objeto/s, la variable de puntero interno m_ptr de este nuevo gestor se hace apuntar a la dirección de memoria a gestionar y el recuento de referencias de la familia aumenta en 1.

Incinerador de basuras

Los punteros inteligentes están diseñados para liberar la memoria que están administrando cuando mueren. En el caso de SharedPtr, asegura que no haya otras referencias a la memoria que se está administrando antes de liberar la memoria. Todo esto sucede en el Destructor del objeto.

Como puede ver en el código, el objeto libera la memoria solo si el recuento de referencias a la memoria es 0, antes de morir.

Esto es importante porque, verá, si un SharedPtr objeto libera la memoria cuando r_count no es 0, otro SharedPtr los objetos que administran la misma memoria intentarían acceder a ella en algún momento y el resultado sería un bloqueo del programa.

los SharedPtr asegura que esto no suceda al otorgar la responsabilidad de liberar memoria al último objeto sobreviviente que está administrando una memoria. Debido al diseño de la SharedPtrtodo esto sucede automáticamente sin la intervención del programador.

Así es como funciona el conteo de referencias.

El conteo de referencias es como la rutina de un par de compañeros de cuarto: el último en salir de la habitación tiene la responsabilidad de cerrar con llave la puerta principal. Para que eso suceda sin problemas, cada compañero de cuarto debe saber si es el último en salir de la habitación.

¿Ha sido útil esta solución?