Domanda shared_from_this causando bad_weak_ptr


Sto cercando di mantenere un elenco di client collegati in asio. Ho adattato l'esempio del server di chat dai documenti (http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/example/cpp03/chat/chat_server.cpp) e qui è la parte importante di ciò che ho finito con:

#include <iostream>
#include <boost/bind.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
#include <set>

using boost::asio::ip::tcp;

class tcp_connection;

std::set<boost::shared_ptr<tcp_connection>> clients;

void add_client(boost::shared_ptr<tcp_connection> client)
{
    clients.insert(client);
}

class tcp_connection : public boost::enable_shared_from_this<tcp_connection>
{
public:
    tcp_connection(boost::asio::io_service& io_service) : socket_(io_service)
    {
    }

    tcp::socket socket_;

    void start()
    {
        add_client(shared_from_this());
    }

    tcp::socket& socket()
    {
        return socket_;
    }
};

class tcp_server
{
public:
    tcp_server(boost::asio::io_service& io_service)
        : io_service_(io_service),
        acceptor_(io_service, tcp::endpoint(tcp::v4(), 6767))
    {
        tcp_connection* new_connection = new tcp_connection(io_service_);
        acceptor_.async_accept(new_connection->socket(),
                             boost::bind(&tcp_server::start_accept, this, new_connection,
                                         boost::asio::placeholders::error));
    }

private:
    void start_accept(tcp_connection* new_connection,
                      const boost::system::error_code& error)
    {
        if (!error)
        {
            new_connection->start();
            new_connection = new tcp_connection(io_service_);
            acceptor_.async_accept(new_connection->socket(),
                                   boost::bind(&tcp_server::start_accept, this, new_connection,
                                               boost::asio::placeholders::error));
        }
    }

    boost::asio::io_service& io_service_;
    tcp::acceptor acceptor_;
};

int main()
{
    try
    {
        boost::asio::io_service io_service;
        tcp_server server(io_service);
        io_service.run();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }

    return 0;
}

Alla chiamata a shared_from_this, il mio server si blocca con il messaggio "Exception: tr1 :: bad_weak_ptr." Ho fatto qualche ricerca e sembra shared_from_this() è piuttosto particolare, ma non riesco a trovare esattamente quello che devo cambiare.


22
2017-12-30 00:24


origine


risposte:


L'analisi essenziale di John Zwinck è azzeccata:

Il bug è che stai usando shared_from_this () su un oggetto che non ha shared_ptr che punta ad esso. Ciò viola una precondizione di shared_from_this (), ovvero che almeno uno shared_ptr deve essere già stato creato (e ancora esiste) che punta a questo.

Tuttavia, il suo consiglio sembra completamente al di là del punto e pericoloso nel codice Asio.

Dovresti risolvere questo - anzi - non gestendo i puntatori grezzi a tcp_connection in primo luogo, ma sempre usando shared_ptr anziché.

boost::bind ha la fantastica caratteristica a cui si lega shared_ptr<> va bene così che mantiene automati- camente l'oggetto puntato all'oggetto vivo fintanto che su di esso opera un'operazione asincrona.

Questo - nel tuo codice di esempio - significa che non hai bisogno del clients vettore, andando dalla parte opposta della risposta di John:

void start_accept()
{
    tcp_connection::sptr new_connection = boost::make_shared<tcp_connection>(io_service_);
    acceptor_.async_accept(new_connection->socket(),
            boost::bind(
                &tcp_server::handle_accept,
                this, new_connection, asio::placeholders::error
            )
        );
}

void handle_accept(tcp_connection::sptr client, boost::system::error_code const& error)
{
    if (!error)
    {
        client->start();
        start_accept();
    }
}

Ho incluso un esempio che rende tcp_connection fai qualche lavoro banale (scorre cicli di scrittura 'ciao mondo' al client ogni secondo, fino a quando il client non rilascia la connessione. Quando lo fa, puoi vedere il distruttore del tcp_connection operazione in corso:

Live On Coliru

#include <iostream>
#include <boost/bind.hpp>
#include <boost/make_shared.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/asio.hpp>
#include <boost/thread.hpp>

namespace asio = boost::asio;
using asio::ip::tcp;

class tcp_connection : public boost::enable_shared_from_this<tcp_connection>
{
public:
    typedef boost::shared_ptr<tcp_connection> sptr;

    tcp_connection(asio::io_service& io_service) : socket_(io_service), timer_(io_service)
    {
    }

    void start()
    {
        std::cout << "Created tcp_connection session\n";

        // post some work bound to this object; if you don't, the client gets
        // 'garbage collected' as the ref count goes to zero
        do_hello();
    }

    ~tcp_connection() {
        std::cout << "Destroyed tcp_connection\n";
    }

    tcp::socket& socket()
    {
        return socket_;
    }

  private:
    tcp::socket socket_;
    asio::deadline_timer timer_;

    void do_hello(boost::system::error_code const& ec = {}) {
        if (!ec) {
            asio::async_write(socket_, asio::buffer("Hello world\n"),
                    boost::bind(&tcp_connection::handle_written, shared_from_this(), asio::placeholders::error, asio::placeholders::bytes_transferred)
                );
        }
    }

    void handle_written(boost::system::error_code const& ec, size_t /*bytes_transferred*/) {
        if (!ec) {
            timer_.expires_from_now(boost::posix_time::seconds(1));
            timer_.async_wait(boost::bind(&tcp_connection::do_hello, shared_from_this(), asio::placeholders::error));
        }
    }
};

class tcp_server
{
public:
    tcp_server(asio::io_service& io_service)
        : io_service_(io_service),
          acceptor_(io_service, tcp::endpoint(tcp::v4(), 6767))
    {
        start_accept();
    }

private:
    void start_accept()
    {
        tcp_connection::sptr new_connection = boost::make_shared<tcp_connection>(io_service_);
        acceptor_.async_accept(new_connection->socket(),
                boost::bind(
                    &tcp_server::handle_accept,
                    this, new_connection, asio::placeholders::error
                )
            );
    }

    void handle_accept(tcp_connection::sptr client, boost::system::error_code const& error)
    {
        if (!error)
        {
            client->start();
            start_accept();
        }
    }

    asio::io_service& io_service_;
    tcp::acceptor acceptor_;
};

int main()
{
    try
    {
        asio::io_service io_service;
        tcp_server server(io_service);

        boost::thread(boost::bind(&asio::io_service::run, &io_service)).detach();

        boost::this_thread::sleep_for(boost::chrono::seconds(4));
        io_service.stop();
    }
    catch (std::exception& e)
    {
        std::cerr << "Exception: " << e.what() << "\n";
    }
}

Uscita tipica:

sehe@desktop:/tmp$ time (./test& (for a in {1..4}; do nc 127.0.0.1 6767& done | nl&); sleep 2; killall nc; wait)
Created tcp_connection session
Created tcp_connection session
     1  Hello world
Created tcp_connection session
     2  Hello world
Created tcp_connection session
     3  Hello world
     4  Hello world
     5  Hello world
     6  Hello world
     7  Hello world
     8  Hello world
     9  Hello world
    10  Hello world
    11  Hello world
    12  Hello world
    13  
Destroyed tcp_connection
Destroyed tcp_connection
Destroyed tcp_connection
Destroyed tcp_connection
Destroyed tcp_connection

real    0m4.003s
user    0m0.000s
sys 0m0.015s

23
2017-12-30 13:01



Il bug è che stai usando shared_from_this() su un oggetto che ha no shared_ptr puntando ad esso. Questo viola una precondizione di shared_from_this(), cioè che almeno uno shared_ptr dovere già sono stati creati (e ancora esistono) che puntano a this.

La causa principale dei tuoi problemi sembra essere il fatto che stai memorizzando il risultato di new inizialmente con un puntatore grezzo. Dovresti memorizzare il risultato di new in un puntatore intelligente (sempre, in pratica). Forse puoi memorizzare il puntatore intelligente nel tuo clients lista subito, quindi.

Un altro approccio che ho menzionato nei commenti è di smettere di usare shared_from_this() interamente. Non ne hai bisogno. Per quanto riguarda questo bit di codice che hai menzionato:

if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
{
    clients.erase(shared_from_this());
}

Puoi sostituirlo con:

if ((boost::asio::error::eof == ec) || (boost::asio::error::connection_reset == ec))
{
    boost::shared_ptr<tcp_connection> victim(this, boost::serialization::null_deleter());
    clients.erase(victim);
}

Cioè, crea un puntatore intelligente "stupido" che non verrà mai deallocato (https://stackoverflow.com/a/5233034/4323) ma che ti darà quello che ti serve per cancellarlo dalla lista dei clienti. Ci sono altri modi per farlo, ad esempio cercando il std::set usando una funzione di confronto che ne prende uno shared_ptr e un puntatore grezzo e sa di confrontare gli indirizzi a cui puntano. Non importa molto quale strada scegli, ma sfuggi al shared_from_this() situazione interamente.


8
2017-12-30 01:20