Aumente las funciones async_ * y shared_ptr’s

Frecuentemente veo este patrón en el código, vinculando shared_from_this como primer parámetro a una función miembro y enviando el resultado usando una función async_* . Aquí hay un ejemplo de otra pregunta:

 void Connection::Receive() { boost::asio::async_read(socket_,boost::asio::buffer(this->read_buffer_), boost::bind(&Connection::handle_Receive, shared_from_this(), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred)); } 

La única razón para usar shared_from_this() lugar de this es mantener el objeto activo hasta que se llame a la función miembro. Pero a menos que haya algún tipo de impulso de magia en alguna parte, dado que this puntero es del tipo Connection* , eso es todo lo que handle_Receive puede tomar, y el puntero inteligente devuelto debe convertirse a un puntero regular inmediatamente. Si eso sucede, no hay nada para mantener vivo el objeto. Y, por supuesto, no hay un puntero al llamar a shared_from_this .

Sin embargo, he visto este patrón tan seguido, no puedo creer que esté tan roto como me parece a mí. ¿Hay alguna magia Boost que hace que shared_ptr se convierta en un puntero regular más tarde, cuando la operación se completa? Si es así, ¿está esto documentado en alguna parte?

En particular, ¿está documentado en alguna parte que el puntero compartido permanecerá en existencia hasta que la operación se complete? Llamar a get_pointer en el puntero fuerte y luego llamar a la función miembro en el puntero devuelto no es suficiente a menos que el puntero fuerte no se destruya hasta que la función miembro regrese.

En resumen, boost::bind crea una copia del boost::shared_ptr que se devuelve desde shared_from_this() , y boost::asio puede crear una copia del controlador. La copia del controlador permanecerá activa hasta que ocurra una de las siguientes situaciones:

  • El controlador ha sido llamado por un hilo desde el que se ha invocado la función de miembro run() , run_one() , poll() o poll_one() servicio.
  • El io_service está destruido.
  • El io_service::service que posee el controlador se cierra a través de shutdown_service() .

Aquí están los extractos relevantes de la documentación:

  • boost :: bind documentation :

    Los argumentos que bind tomas son copiados y retenidos internamente por el objeto de función devuelto.

  • boost :: asio io_service::post :

    El io_service garantiza que el controlador solo se io_service en un hilo en el que se invocan actualmente las funciones miembro run() , run_one() , poll() o poll_one() . […] El io_service hará una copia del objeto controlador según sea necesario.

  • boost :: asio io_service::~io_service :

    Los objetos del manejador no invocado que fueron progtwigdos para la invocación diferida en el io_service , o cualquier cadena asociada, se destruyen.

    Cuando la duración de un objeto está ligada a la duración de una conexión (o de alguna otra secuencia de operaciones asincrónicas), un shared_ptr del objeto se vinculará a los manejadores para todas las operaciones asíncronas asociadas con él. […] Cuando termina una conexión única, se completan todas las operaciones asincrónicas asociadas. Los objetos correspondientes del manejador se destruyen y todas shared_ptr referencias shared_ptr a los objetos se destruyen.


Aunque está fechado (2007), la Propuesta de Biblioteca de Networking para TR2 (Revisión 1) se derivó de Boost.Asio. Sección 5.3.2.7. Requirements on asynchronous operations 5.3.2.7. Requirements on asynchronous operations proporcionan algunos detalles para los argumentos para async_ funciones async_ :

En esta cláusula, una función que se nombra con el prefijo async_ inicia una operación asíncrona. Estas funciones se conocerán como funciones iniciadoras . […] La implementación de la biblioteca puede hacer copias del argumento del manejador, y el argumento del manejador original y todas las copias son intercambiables.

La duración de los argumentos para iniciar las funciones se tratará de la siguiente manera:

  • Si el parámetro se declara como referencia constante o como valor por […] valor, la implementación puede hacer copias del argumento y todas las copias se destruirán a más tardar inmediatamente después de la invocación del controlador.

[…] Cualquier llamada realizada por la implementación de la biblioteca a funciones asociadas con los argumentos de la función iniciadora se realizará de tal manera que las llamadas ocurran en una llamada de secuencia 1 para llamar a n , donde para todo i , 1 ≤ i < n , la llamada precede llama a i + 1 .

Así:

  • La implementación puede crear una copia del controlador . En el ejemplo, el controlador copiado creará una copia de shared_ptr , lo que aumenta el recuento de referencias de la instancia de Connection mientras las copias del controlador permanecen activas.
  • La implementación puede destruir el controlador antes de invocar el controlador . Esto ocurre si la operación de io_serive::service está pendiente cuando io_serive::service se io_service o se destruye el io_service . En el ejemplo, las copias del controlador se destruirán, lo que disminuirá el recuento de referencias de Connection y podría causar la destrucción de la instancia de Connection .
  • Si se invoca el controlador , todas las copias del controlador se destruirán inmediatamente una vez que el manejador devuelva la ejecución. Nuevamente, las copias del controlador serán destruidas, disminuyendo el recuento de referencia de Connection , y potencialmente causando su destrucción.
  • Las funciones asociadas con los argumentos de asnyc_ , se ejecutarán secuencialmente y no simultáneamente. Esto incluye io_handler_deallocate y io_handler_invoke . Esto garantiza que el controlador no será desasignado mientras se invoca el controlador . En la mayoría de las áreas de la implementación de boost::asio , el controlador se copia o se mueve a las variables de la stack, lo que permite la destrucción que se produce una vez que la ejecución sale del bloque en el que se declaró. En el ejemplo, esto asegura que el recuento de referencia para Connection será al menos uno durante la invocación del controlador .

Dice así:

1) La documentación de Boost.Bind establece :

“[Nota: mem_fn crea objetos de función que pueden aceptar un puntero, una referencia o un puntero inteligente a un objeto como su primer argumento; para obtener información adicional, consulte la documentación de mem_fn.]”

2) La documentación de mem_fn dice :

Cuando el objeto de función se invoca con un primer argumento x que no es ni un puntero ni una referencia a la clase apropiada (X en el ejemplo anterior), usa get_pointer (x) para obtener un puntero de x. Los autores de la biblioteca pueden “registrar” sus clases de punteros inteligentes mediante el suministro de una sobrecarga get_pointer apropiada, lo que permite a mem_fn reconocerlos y respaldarlos.

Por lo tanto, el puntero o el puntero inteligente se almacenan en el archivador tal como está, hasta su invocación.

También veo que este patrón se usa mucho y (gracias a @Tanner) puedo ver por qué se usa cuando el io_service se ejecuta en múltiples hilos . Sin embargo, creo que todavía existen problemas de por vida, ya que reemplaza una falla potencial con una potencial fuga de memoria / recursos …

Gracias a boost :: bind, las devoluciones de llamadas que están asociadas a shared_ptrs se convierten en “usuarios” del objeto (aumentando los objetos use_count), por lo que el objeto no se eliminará hasta que se hayan llamado a todas las devoluciones de llamada pendientes.

Las llamadas a las funciones boost :: asio :: async * se llaman cada vez que se invoca cancelar o cerrar en el temporizador o socket correspondiente. Por lo general, solo debe realizar las llamadas de cancelación / cierre apropiadas en el destructor utilizando el patrón de RAII amado de Stroustrup; trabajo hecho.

Sin embargo, no se invocará al destructor cuando el propietario elimine el objeto, ya que las devoluciones de llamada todavía contienen copias de los shared_ptrs, por lo que su use_count será mayor que cero, lo que generará una pérdida de recursos. La fuga se puede evitar realizando las llamadas de cancelación / cierre apropiadas antes de eliminar el objeto. Pero no es tan infalible como usar RAII y realizar cancelaciones / cerrar llamadas en el destructor. Asegurar que los recursos siempre se liberen, incluso en presencia de excepciones.

Un patrón conforme RAII es usar funciones estáticas para callbacks y pasar un weak_ptr para impulsar :: bind al registrar la función de callback como en el ejemplo a continuación:

 class Connection : public boost::enable_shared_from_this { boost::asio::ip::tcp::socket socket_; boost::asio::strand strand_; /// shared pointer to a buffer, so that the buffer may outlive the Connection boost::shared_ptr > read_buffer_; void read_handler(boost::system::error_code const& error, size_t bytes_transferred) { // process the read event as usual } /// Static callback function. /// It ensures that the object still exists and the event is valid /// before calling the read handler. static void read_callback(boost::weak_ptr ptr, boost::system::error_code const& error, size_t bytes_transferred, boost::shared_ptr > /* read_buffer */) { boost::shared_ptr pointer(ptr.lock()); if (pointer && (boost::asio::error::operation_aborted != error)) pointer->read_handler(error, bytes_transferred); } /// Private constructor to ensure the class is created as a shared_ptr. explicit Connection(boost::asio::io_service& io_service) : socket_(io_service), strand_(io_service), read_buffer_(new std::vector()) {} public: /// Factory method to create an instance of this class. static boost::shared_ptr create(boost::asio::io_service& io_service) { return boost::shared_ptr(new Connection(io_service)); } /// Destructor, closes the socket to cancel the read callback (by /// calling it with error = boost::asio::error::operation_aborted) and /// free the weak_ptr held by the call to bind in the Receive function. ~Connection() { socket_.close(); } /// Convert the shared_ptr to a weak_ptr in the call to bind void Receive() { boost::asio::async_read(socket_, boost::asio::buffer(read_buffer_), strand_.wrap(boost::bind(&Connection::read_callback, boost::weak_ptr(shared_from_this()), boost::asio::placeholders::error, boost::asio::placeholders::bytes_transferred, read_buffer_))); } }; 

Nota: read_buffer_ se almacena como shared_ptr en la clase Connection y se pasa a la función read_callback como shared_ptr .

Esto es para garantizar que cuando se ejecutan varios io_services en tareas separadas, el read_buffer_ no se elimine hasta que se completen las otras tareas, es decir, cuando se haya llamado a la función read_callback .

No hay conversión de boost::shared_ptr (el tipo de shared_from_this de shared_from_this ) a Connection* (el tipo de this ), ya que sería inseguro como lo señaló legítimamente.

La magia está en Boost.Bind. Para decirlo simplemente, en una llamada del formulario bind(f, a, b, c) (sin marcador de posición o expresión de enlace anidada involucrada para este ejemplo) donde f es un puntero al miembro, entonces se generará una llamada al resultado de la llamada en una llamada de la forma (a.*f)(b, c) si a tiene un tipo derivado del tipo de clase del puntero al miembro (o tipo boost::reference_wrapper ), o si no tiene la forma ((*a).*f)(b, c) . Esto funciona con punteros y punteros inteligentes por igual. (En realidad estoy trabajando desde la memoria las reglas para std::bind , Boost.Bind no es exactamente idéntico pero ambos tienen el mismo espíritu).

Además, el resultado de shared_from_this() se almacena en el resultado de la llamada para bind , lo que garantiza que no haya problemas de por vida.

Tal vez me falta algo obvio aquí, pero el shared_ptr devuelto por shared_from_this() se almacena en el objeto de función devuelto por boost::bind , que lo mantiene activo. Solo se convierte implícitamente a Connection* en el momento en que se inicia la callback cuando finaliza la lectura asíncrona, y el objeto se mantiene activo durante al menos la duración de la llamada. Si el handle_Receive no crea otro shared_ptr a partir de esto, y el shared_ptr que se almacenó en el enlace functor es el último shared_ptr vivo, el objeto se destruirá después de que la callback vuelva.