web-dev-qa-db-fra.com

shared_from_this causant bad_weak_ptr

J'essaie de garder une liste des clients connectés dans asio. J'ai adapté l'exemple de serveur de chat à partir des documents ( http://www.boost.org/doc/libs/1_57_0/doc/html/boost_asio/example/cpp03/chat/chat_server.cpp ) et voici la partie importante de ce que j'ai fini avec:

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

Lors de l'appel à shared_from_this, mon serveur se bloque avec le message "Exception: tr1 :: bad_weak_ptr." J'ai fait quelques recherches et il semble que shared_from_this() soit assez particulier, mais je n'arrive pas à trouver exactement ce que je dois changer.

39
chrisvj

L'analyse essentielle de John Zwinck porte sur:

Le bogue est que vous utilisez shared_from_this () sur un objet qui n'a pas de shared_ptr pointant vers lui. Cela viole une condition préalable de shared_from_this (), à savoir qu'au moins un shared_ptr doit déjà avoir été créé (et existe toujours) pointant vers cela.

Cependant, ses conseils semblent complètement hors de propos et dangereux dans le code Asio.

Vous devriez résoudre ce problème en ne manipulant pas de pointeurs bruts vers tcp_connection en premier lieu mais toujours en utilisant shared_ptr au lieu.

boost::bind possède la fonctionnalité impressionnante à laquelle il se lie shared_ptr<> très bien, donc il maintient automatiquement l'objet pointé en vie tant qu'une opération asynchrone fonctionne sur lui.

Cela - dans votre exemple de code - signifie que vous n'avez pas besoin du vecteur clients, dans le sens inverse de la réponse de 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();
    }
}

J'ai inclus un échantillon qui fait tcp_connection faire un travail trivial (il boucle l'écriture de 'hello world' au client chaque seconde, jusqu'à ce que le client abandonne la connexion. Quand il le fait, vous pouvez voir le destructeur du tcp_connection opération en cours d'exécution:

Live On Colir

#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";
    }
}

Sortie typique:

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
38
sehe

Le bogue est que vous utilisez shared_from_this() sur un objet qui ne pointe pas sur shared_ptr. Cela viole une condition préalable de shared_from_this(), à savoir qu'au moins un shared_ptr Doit déjà avoir été créé (et encore existent) pointant vers this.

La cause première de vos problèmes semble être le fait que vous stockez le résultat de new dans un pointeur brut au départ. Vous devez stocker le résultat de new dans un pointeur intelligent (toujours, fondamentalement). Vous pouvez peut-être alors stocker immédiatement le pointeur intelligent dans votre liste clients.

Une autre approche que j'ai mentionnée dans les commentaires consiste à cesser complètement d'utiliser shared_from_this(). Vous n'en avez pas besoin. Quant à ce morceau de code que vous avez mentionné:

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

Vous pouvez le remplacer par:

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

Autrement dit, créez un pointeur intelligent "stupide" qui ne se désallouera jamais ( https://stackoverflow.com/a/5233034/432 ) mais qui vous donnera ce dont vous avez besoin pour le supprimer de la liste de clients. Il y a d'autres façons de le faire aussi, comme en recherchant le std::set En utilisant une fonction de comparaison qui prend un shared_ptr Et un pointeur brut et sait comparer les adresses vers lesquelles ils pointent. Peu importe la façon dont vous choisissez, mais vous échappez complètement à la situation shared_from_this().

11
John Zwinck
// Do not forget to ----v---- publicly inherit :)
class tcp_connection : public boost::enable_shared_from_this<tcp_connection>
0
franckspike