c++ - verdad - tag de youtube
Boost async_*funciones y shared_ptr''s (5)
Dice así:
1) La documentación de Boost.Bind states :
"[Nota: mem_fn crea objetos de función que pueden aceptar un puntero, una referencia o un puntero inteligente a un objeto como primer argumento; para obtener información adicional, consulte la documentación de mem_fn.]"
2) la documentación de mem_fn says :
Cuando se invoca el objeto de función con un primer argumento x que no es 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 bibliotecas pueden "registrar" sus clases de puntero inteligente proporcionando una sobrecarga apropiada de get_pointer, lo que permite que mem_fn los reconozca y los respalde.
Por lo tanto, el puntero o puntero inteligente se almacena en el archivador tal como está, hasta su invocación.
Con frecuencia veo este patrón en el código, vinculando shared_from_this
como el 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 vivo hasta que se llame a la función miembro. Pero a menos que haya algún tipo de magia de impulso en algún lugar, ya que this
puntero es del tipo Connection*
, eso es todo handle_Receive
puede tomar handle_Receive
, y el puntero inteligente devuelto debe convertirse inmediatamente en un puntero normal. Si eso sucede, no hay nada para mantener vivo el objeto. Y, por supuesto, no hay un puntero en llamar a shared_from_this
.
Sin embargo, he visto este patrón tan a menudo, no puedo creer que esté tan completamente roto como me parece. ¿Hay alguna magia Boost que haga que shared_ptr se convierta en un puntero regular más tarde, cuando se complete la operación? Si es así, ¿está esto documentado en alguna parte?
En particular, ¿se documenta en algún lugar que el puntero compartido seguirá existiendo hasta que se complete la operación? Llamar a get_pointer
en el puntero get_pointer
y luego llamar a la función miembro en el puntero devuelto no es suficiente a menos que el puntero no se destruya hasta que la función miembro regrese.
En resumen, boost::bind
crea una copia de boost::shared_ptr<Connection>
que se devuelve de shared_from_this()
, y boost::asio
puede crear una copia del controlador. La copia del manejador permanecerá activa hasta que ocurra uno de los siguientes:
- El controlador ha sido llamado por un subproceso desde el que se ha invocado la función miembro
run()
,run_one()
,poll()
opoll_one()
servicio. - El
io_service
está destruido. - El
io_service::service
que posee el controlador se apaga a través deshutdown_service()
.
Aquí están los extractos relevantes de la documentación:
boost :: enlazar documentation :
Los argumentos que toman las vinculaciones son copiados y mantenidos internamente por el objeto de función devuelto.
boost :: asio
io_service::post
:El
io_service
garantiza que solo se llamará al manejador en un subproceso en el que se estén invocando las funciones miembrorun()
,run_one()
,poll()
opoll_one()
. [...] Elio_service
hará una copia del objeto controlador según sea necesario.boost :: asio
io_service::~io_service
:Los objetos de controlador no invocados que se programaron para invocación diferida en el servicio
io_service
, o cualquier hebra asociada, se destruyen.
Cuando la vida útil de un objeto está vinculada a la vida útil de una conexión (o alguna otra secuencia de operaciones asíncronas), unashared_ptr
al objeto se vincularía a los controladores para todas las operaciones asíncronas asociadas a él. [...] Cuando finaliza una sola conexión, se completan todas las operaciones asíncronas asociadas. Los objetos de controlador correspondientes se destruyen y todasshared_ptr
referencias deshared_ptr
a los objetos se destruyen.
Aunque está fechado (2007), la propuesta de Networking Library 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 de async_
funciones async_
:
En esta cláusula, una operación que se denomina con el prefijo
async_
inicia una operación asíncrona. Estas funciones serán conocidas como funciones iniciadoras . [...] La implementación de la biblioteca puede hacer copias del argumento del controlador, y el argumento del controlador original y todas las copias son intercambiables.La duración de los argumentos para iniciar funciones se tratará de la siguiente manera:
- Si el parámetro se declara como una referencia constante o 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.
[...] Todas las llamadas realizadas por la implementación de la biblioteca a las funciones asociadas con los argumentos de la función de inicio se realizarán de modo que las llamadas se realicen en una secuencia llamada 1 a n , donde para todos i , 1 ≤ i < n , la llamada i precede llamar a i + 1 .
Así:
- La implementación puede crear una copia del controlador . En el ejemplo, el controlador copiado creará una copia de la
shared_ptr<Connection>
, aumentando el recuento de referencia de la instancia deConnection
mientras las copias del controlador permanezcan vivas. - La implementación puede destruir el controlador antes de invocar el controlador . Esto ocurre si la operación asíncrona está pendiente cuando
io_serive::service
se apaga o se destruye elio_service
. En el ejemplo, las copias del controlador se destruirán, lo que disminuirá el recuento de referencia deConnection
y, potencialmente, causará que la instancia deConnection
se destruya. - Si se invoca el controlador , todas las copias del controlador se destruirán inmediatamente una vez que la ejecución vuelva del controlador. Nuevamente, las copias del controlador se destruirán, lo que disminuirá el recuento de referencia de
Connection
y, potencialmente, causará que se destruya. - Las funciones asociadas con los argumentos de
asnyc_
, se ejecutarán de forma secuencial, y no concurrentes. Esto incluyeio_handler_deallocate
yio_handler_invoke
. Esto garantiza que el manejador no se desasignará mientras se invoca el manejador . En la mayoría de las áreas de la implementaciónboost::asio
, el controlador se copia o se mueve para apilar variables, lo que permite que se produzca la destrucción una vez que la ejecución sale del bloque en el que se declaró. En el ejemplo, esto garantiza que el recuento de referencia para laConnection
será al menos uno durante la invocación del controlador .
No hay conversión de boost::shared_ptr<Connection>
(el tipo de retorno de shared_from_this
) a Connection*
(el tipo de this
), ya que sería inseguro como lo señalaste acertadamente.
La magia está en Boost.Bind. En pocas palabras, en una llamada del bind(f, a, b, c)
formulario bind(f, a, b, c)
(sin marcador de posición o expresión de enlace anidada involucrada en este ejemplo), donde f
es un puntero a miembro, luego se obtendrá el resultado de la llamada. en una llamada del formulario (a.*f)(b, c)
si a
tiene un tipo derivado del tipo de clase del puntero al miembro (o escriba boost::reference_wrapper<U>
), o de lo contrario es de la forma ((*a).*f)(b, c)
. Esto funciona con punteros y punteros inteligentes por igual. (De hecho, estoy trabajando de memoria las reglas para std::bind
, Boost.Bind no es exactamente idéntico, pero ambas están en el mismo espíritu).
Además, el resultado de shared_from_this()
se almacena en el resultado de la llamada a bind
, asegurando que no haya problemas de por vida.
Tal vez me esté perdiendo algo obvio aquí, pero shared_ptr devuelto por shared_from_this()
se almacena en la función objeto devuelta por boost::bind
, que la mantiene viva. Solo se convierte de forma implícita a Connection*
en el momento en que se inicia la devolución de llamada cuando finaliza la lectura asíncrona, y el objeto se mantiene vivo 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 functor de vinculación es el último shared_ptr activo, el objeto se destruirá después de que vuelva la devolución de llamada.
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 varios subprocesos . Sin embargo, creo que todavía hay problemas de por vida, ya que reemplaza una falla potencial con una pérdida potencial de memoria / recursos ...
Gracias a boost :: bind, las devoluciones de llamada que están vinculadas a shared_ptrs se convierten en "usuarios" del objeto (lo que aumenta el uso de los objetos), por lo que el objeto no se eliminará hasta que se hayan llamado todas las devoluciones de llamada pendientes.
Las devoluciones de llamada a las funciones boost :: asio :: async * se llaman cuando se llama a cancel o close en el temporizador o socket correspondiente. Normalmente, solo haría las llamadas de cancelación / cierre apropiadas en el destructor utilizando el amado patrón RAII Stroustrup; trabajo hecho.
Sin embargo, no se llamará al destructor cuando el propietario elimine el objeto, ya que las devoluciones de llamada aún contienen copias de shared_ptrs y, por lo tanto, su use_count será mayor que cero, lo que provocará una fuga de recursos. La fuga puede evitarse haciendo las llamadas de cancelación / cierre apropiadas antes de eliminar el objeto. Pero no es tan infalible como usar RAII y hacer las llamadas de cancelar / cerrar en el destructor. Asegurar que los recursos se liberen siempre , incluso en presencia de excepciones.
Un patrón conforme a RAII es usar funciones estáticas para devoluciones de llamada y pasar un weak_ptr para aumentar :: bind cuando se registra la función de devolución de llamada como en el siguiente ejemplo:
class Connection : public boost::enable_shared_from_this<Connection>
{
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<std::vector<char> > 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<Connection> ptr,
boost::system::error_code const& error,
size_t bytes_transferred,
boost::shared_ptr<std::vector<char> > /* read_buffer */)
{
boost::shared_ptr<Connection> 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<char>())
{}
public:
/// Factory method to create an instance of this class.
static boost::shared_ptr<Connection> create(boost::asio::io_service& io_service)
{ return boost::shared_ptr<Connection>(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<Connection>(shared_from_this()),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred,
read_buffer_)));
}
};
Nota: el 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 múltiples io_services
en tareas separadas, el read_buffer_
no se elimine hasta después de que las otras tareas se hayan completado, es decir, cuando se haya llamado a la función read_callback
.