Aumenta le funzioni async_ * e shared_ptr’s

Vedo spesso questo modello in codice, vincolando shared_from_this come primo parametro a una funzione membro e async_* il risultato usando una funzione async_* . Ecco un esempio da un’altra domanda:

 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)); } 

L’unica ragione per usare shared_from_this() invece di this è mantenere l’object vivo fino a quando la funzione membro viene chiamata. Ma a meno che non ci sia un qualche tipo di magia boost da qualche parte, dato che this puntatore è di tipo Connection* , tutto handle_Receive che handle_Receive può prendere e il puntatore intelligente restituito dovrebbe essere convertito immediatamente in un puntatore normale. Se ciò accade, non c’è nulla per mantenere vivo l’object. E, naturalmente, non c’è nessun puntatore nel chiamare shared_from_this .

Tuttavia, ho visto questo modello così spesso, non posso credere che sia completamente rotto come sembra a me. C’è qualche magia di potenziamento che fa sì che il parametro shared_ptr venga convertito in un puntatore normale in seguito, quando l’operazione viene completata? Se è così, è documentato da qualche parte?

In particolare, è documentato da qualche parte che il puntatore condiviso rimarrà in esistenza fino al completamento dell’operazione? Chiamare get_pointer sul puntatore forte e quindi chiamare la funzione membro sul puntatore restituito non è sufficiente a meno che il puntatore non venga eliminato finché non viene restituita la funzione membro.

In breve, boost::bind crea una copia di boost::shared_ptr che viene restituita da shared_from_this() , e boost::asio può creare una copia del gestore. La copia del gestore rimarrà in vita fino a quando si verifica una delle seguenti condizioni:

  • Il gestore è stato chiamato da un thread da cui è stata richiamata la funzione membro run() , run_one() , poll() o poll_one() .
  • Il servizio io_service è distrutto.
  • Il io_service::service proprietario del gestore viene arrestato tramite shutdown_service() .

Ecco gli estratti rilevanti dalla documentazione:

  • boost :: documentazione di bind:

    Gli argomenti bind vengono copiati e conservati internamente dall’object function restituito.

  • boost :: asio io_service::post :

    io_service garantisce che il gestore venga chiamato solo in un thread in cui al momento vengono invocate le funzioni membro run() , run_one() , poll_one() o poll_one() . […] io_service creerà una copia dell’object gestore come richiesto.

  • boost :: asio io_service::~io_service :

    Gli oggetti gestore non inviati pianificati per il richiamo posticipato su io_service o su qualsiasi filo associato vengono distrutti.

    Laddove la durata di un object è legata alla durata di una connessione (o qualche altra sequenza di operazioni asincrone), un shared_ptr all’object viene associato ai gestori per tutte le operazioni asincrone ad esso associate. […] Quando termina una singola connessione, vengono completate tutte le operazioni asincrone associate. Gli oggetti corrispondenti del gestore vengono distrutti e tutti i riferimenti shared_ptr agli oggetti vengono distrutti.


Mentre datato (2007), la proposta di libreria di rete per TR2 (Revisione 1) è stata derivata da Boost.Asio. Sezione 5.3.2.7. Requirements on asynchronous operations 5.3.2.7. Requirements on asynchronous operations forniscono alcuni dettagli per gli argomenti async_ funzioni async_ :

In questa clausola, un’operazione asincrona viene avviata da una funzione che viene denominata con il prefisso async_ . Queste funzioni devono essere note come funzioni iniziali . […] L’implementazione della libreria può fare copie dell’argomento del gestore e l’argomento del gestore originale e tutte le copie sono intercambiabili.

La durata degli argomenti per l’avvio delle funzioni deve essere trattata come segue:

  • Se il parametro è dichiarato come riferimento const o valore […] l’implementazione può fare copie dell’argomento e tutte le copie devono essere distrutte non più tardi immediatamente dopo l’invocazione del gestore.

[…] Qualsiasi chiamata effettuata dall’implementazione della libreria alle funzioni associate agli argomenti della funzione di avvio verrà eseguita in modo tale che le chiamate si verifichino in una chiamata di sequenza 1 alla chiamata n , dove per tutti i , 1 ≤ i < n , la chiamata i precede chiama i + 1 .

Così:

  • L’implementazione può creare una copia del gestore . Nell’esempio, il gestore copiato creerà una copia di shared_ptr , aumentando il conteggio dei riferimenti dell’istanza di Connection mentre le copie del gestore rimangono attive.
  • L’implementazione può distruggere il gestore prima di invocare il gestore . Ciò si verifica se l’operazione asincrona è in sospeso quando io_serive::service è arrestato o il servizio io_service è distrutto. Nell’esempio, le copie del gestore verranno distrutte, diminuendo il conteggio dei riferimenti di Connection e provocando potenzialmente la distruzione dell’istanza di Connection .
  • Se viene richiamato il gestore , tutte le copie del gestore verranno immediatamente distrutte una volta che l’esecuzione ritorna dal gestore. Di nuovo, le copie del gestore verranno distrutte, riducendo il numero di riferimenti di Connection e potenzialmente causando la distruzione.
  • Le funzioni associate agli argomenti di asnyc_ verranno eseguite in sequenza e non in concomitanza. Questo include io_handler_deallocate e io_handler_invoke . Ciò garantisce che il gestore non venga deallocato mentre il gestore viene invocato. Nella maggior parte delle aree di implementazione boost::asio , il gestore viene copiato o spostato nello stack delle variabili, consentendo la distruzione che si verifica una volta che l’esecuzione esce dal blocco in cui è stata dichiarata. Nell’esempio, ciò garantisce che il conteggio dei riferimenti per Connection sia almeno uno durante il richiamo del gestore .

Va così:

1) Gli stati della documentazione Boost.Bind:

“[Nota: mem_fn crea oggetti funzione che sono in grado di accettare un puntatore, un riferimento o un puntatore intelligente su un object come primo argomento; per ulteriori informazioni, consultare la documentazione di mem_fn.]”

2) La documentazione di mem_fn dice :

Quando l’object funzione viene richiamato con un primo argomento x che non è né un puntatore né un riferimento alla class appropriata (X nell’esempio precedente), utilizza get_pointer (x) per ottenere un puntatore da x. Gli autori di librerie possono “registrare” le loro classi di puntatori intelligenti fornendo un opportuno sovraccarico get_pointer, consentendo a mem_fn di riconoscerli e supportarli.

Quindi, puntatore o puntatore intelligente viene memorizzato nel raccoglitore così com’è, fino alla sua chiamata.

Vedo anche che questo pattern è molto usato e (grazie a @Tanner) posso capire perché viene usato quando io_service viene eseguito in più thread . Tuttavia, penso che ci siano ancora problemi di durata con esso in quanto sostituisce un potenziale incidente con una potenziale perdita di memoria / risorse …

Grazie a boost :: bind, ogni callback associato a shared_ptrs diventa “utente” dell’object (aumentando gli oggetti use_count), quindi l’object non verrà eliminato finché non saranno stati richiamati tutti i callback in sospeso.

I richiami delle funzioni boost :: asio :: async * vengono richiamati ogni volta che si annulla o si chiude sul timer o sul socket pertinente. Normalmente si farebbe semplicemente l’annullamento / chiusura delle chiamate appropriate nel distruttore usando l’amato pattern RAII di Stroustrup; lavoro fatto.

Tuttavia, il distruttore non verrà chiamato quando il proprietario elimina l’object, poiché i callback conservano ancora copie di shared_ptrs e quindi il loro use_count sarà maggiore di zero, causando una perdita di risorse. La perdita può essere evitata effettuando le chiamate di annullamento / chiusura appropriate prima di eliminare l’object. Ma non è così infallibile come usare RAII e fare le chiamate di annullamento / chiusura nel distruttore. Garantire che le risorse vengano sempre liberate, anche in presenza di eccezioni.

Un pattern conforms di RAII consiste nell’usare funzioni statiche per i callback e passare un weak_ptr a boost :: bind quando si registra la funzione di callback come nell’esempio seguente:

 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_ è memorizzato come shared_ptr nella class Connection e passato alla funzione read_callback come shared_ptr .

Questo per garantire che laddove vengono eseguiti più io_services in attività separate, read_buffer_ non viene eliminato fino a quando non sono state completate le altre attività, ovvero quando è stata richiamata la funzione read_callback .

Non c’è conversione da boost::shared_ptr (il tipo di ritorno di shared_from_this ) a Connection* (il tipo di this ), in quanto sarebbe pericoloso come hai giustamente sottolineato.

La magia è in Boost.Bind. Per dirla semplicemente, in una chiamata del modulo bind(f, a, b, c) (nessun segnaposto o espressione di bind nested coinvolta per questo esempio) dove f è un puntatore al membro, quindi il risultato della chiamata risulterà in una chiamata del modulo (a.*f)(b, c) se a ha un tipo derivato dal tipo di class del puntatore al membro (o tipo boost::reference_wrapper ), oppure è del modulo ((*a).*f)(b, c) . Funziona allo stesso modo con puntatori e puntatori intelligenti. (In realtà sto lavorando dalla memoria alle regole per std::bind , Boost.Bind non è esattamente identico, ma entrambi sono nello stesso spirito.)

Inoltre, il risultato di shared_from_this() è memorizzato nel risultato della chiamata a bind , assicurando che non ci siano problemi di durata.

Forse mi manca qualcosa di ovvio, ma il parametro shared_ptr restituito da shared_from_this() è memorizzato nell’object function restituito da boost::bind , che lo mantiene attivo. Viene convertito solo in modo implicito in Connection* nel momento in cui la richiamata viene avviata al termine della lettura asincrona e l’object viene mantenuto attivo per almeno la durata della chiamata. Se handle_Receive non crea un altro shared_ptr da questo, e il shared_ptr che è stato memorizzato nel bind functor è l’ultimo shared_ptr attivo, l’object verrà distrutto dopo il ritorno del callback.