Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Boost async_* functions and shared_ptr's

Tags:

I frequently see this pattern in code, binding shared_from_this as the first parameter to a member function and dispatching the result using an async_* function. Here's an example from another question:

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

The only reason to use shared_from_this() instead of this is to keep the object alive until the member function gets called. But unless there's some kind of boost magic somewhere, since the this pointer is of type Connection*, that's all handle_Receive can take, and the smart pointer returned should be converted to a regular pointer immediately. If that happens, there's nothing to keep the object alive. And, of course, there's no pointer in calling shared_from_this.

However, I've seen this pattern so often, I can't believe it's as completely broken as it seems to me. Is there some Boost magic that causes the shared_ptr to be converted to a regular pointer later, when the operation completes? If so, is this documented somewhere?

In particular, is it documented somewhere that the shared pointer will remain in existence until the operation completes? Calling get_pointer on the strong pointer and then calling the member function on the returned pointer is not sufficient unless the strong pointer isn't destroyed until the member function returns.

like image 289
David Schwartz Avatar asked Jul 06 '12 06:07

David Schwartz


People also ask

What is boost :: shared_ptr?

shared_ptr is now part of the C++11 Standard, as std::shared_ptr . Starting with Boost release 1.53, shared_ptr can be used to hold a pointer to a dynamically allocated array. This is accomplished by using an array type ( T[] or T[N] ) as the template parameter.

What is shared_ptr used for?

The shared_ptr type is a smart pointer in the C++ standard library that is designed for scenarios in which more than one owner might have to manage the lifetime of the object in memory.

Is shared_ptr slow?

Admittedly, the std::shared_ptr is about two times slower than new and delete. Even std::make_shared has a performance overhead of about 10%.

Do I need to delete a shared_ptr?

The purpose of shared_ptr is to manage an object that no one "person" has the right or responsibility to delete, because there could be others sharing ownership. So you shouldn't ever want to, either.


3 Answers

In short, boost::bind creates a copy of the boost::shared_ptr<Connection> that is returned from shared_from_this(), and boost::asio may create a copy of the handler. The copy of the handler will remain alive until one of the following occurs:

  • The handler has been called by a thread from which the service's run(), run_one(), poll() or poll_one() member function has been invoked.
  • The io_service is destroyed.
  • The io_service::service that owns the handler is shutdown via shutdown_service().

Here are the relevant excerpts from the documentation:

  • boost::bind documentation:

    The arguments that bind takes are copied and held internally by the returned function object.

  • boost::asio io_service::post:

    The io_service guarantees that the handler will only be called in a thread in which the run(), run_one(), poll() or poll_one() member functions is currently being invoked. [...] The io_service will make a copy of the handler object as required.

  • boost::asio io_service::~io_service:

    Uninvoked handler objects that were scheduled for deferred invocation on the io_service, or any associated strand, are destroyed.

    Where an object's lifetime is tied to the lifetime of a connection (or some other sequence of asynchronous operations), a shared_ptr to the object would be bound into the handlers for all asynchronous operations associated with it. [...] When a single connection ends, all associated asynchronous operations complete. The corresponding handler objects are destroyed, and all shared_ptr references to the objects are destroyed.


While dated (2007), the Networking Library Proposal for TR2 (Revision 1) was derived from Boost.Asio. Section 5.3.2.7. Requirements on asynchronous operations provides some details for the arguments to async_ functions:

In this clause, an asynchronous operation is initiated by a function that is named with the prefix async_. These functions shall be known as initiating functions. [...] The library implementation may make copies of the handler argument, and the original handler argument and all copies are interchangeable.

The lifetime of arguments to initiating functions shall be treated as follows:

  • If the parameter is declared as a const reference or by-value [...] the implementation may make copies of the argument, and all copies shall be destroyed no later than immediately after invocation of the handler.

[...] Any calls made by the library implementation to functions associated with the initiating function's arguments will be performed such that calls occur in a sequence call1 to calln, where for all i, 1 ≤ i < n, calli precedes call i+1.

Thus:

  • The implementation may create a copy of the handler. In the example, the copied handler will create a copy of the shared_ptr<Connection>, increasing the reference count of the Connection instance while the copies of handler remain alive.
  • The implementation may destroy the handler prior to invoking handler. This occurs if the async operation is outstanding when io_serive::service is shutdown or the io_service is destroyed. In the example, the copies of handler will be destroyed, decreasing the reference count of Connection, and potentially causing the Connection instance to be destroyed.
  • If handler is invoked, then all copies of handler will immediately be destroyed once execution returns from the handler. Again, the copies of handler will be destroyed, decreasing the reference count of Connection, and potentially causing it to be destroyed.
  • The functions associated with the asnyc_'s arguments, will be executed sequentially, and not concurrent. This includes io_handler_deallocate and io_handler_invoke. This guarantees that the handler will not be deallocated while the handler is being invoked. In most areas of the boost::asio implementation, the handler is copied or moved to stack variables, allowing the destruction to occur once execution exits the block in which it was declared. In the example, this ensures that the reference count for Connection will be at least one during the invocation of the handler.
like image 95
Tanner Sansbury Avatar answered Sep 22 '22 16:09

Tanner Sansbury


It goes like this:

1) Boost.Bind documentation states:

"[Note: mem_fn creates function objects that are able to accept a pointer, a reference, or a smart pointer to an object as its first argument; for additional information, see the mem_fn documentation.]"

2) mem_fn documentation says:

When the function object is invoked with a first argument x that is neither a pointer nor a reference to the appropriate class (X in the example above), it uses get_pointer(x) to obtain a pointer from x. Library authors can "register" their smart pointer classes by supplying an appropriate get_pointer overload, allowing mem_fn to recognize and support them.

So, pointer or smart-pointer is stored in the binder as-is, until its invocation.

like image 33
Igor R. Avatar answered Sep 21 '22 16:09

Igor R.


I also see this pattern used a lot and (thanks to @Tanner) I can see why it's used when the io_service is run in multiple threads. However, I think that there are still lifetime issues with it as it replaces a potential crash with a potential memory/resource leak...

Thanks to boost::bind, any callbacks that are bound to shared_ptrs become "users" of the object (increasing the objects use_count), so the object won't be deleted until all of the outstanding callbacks have been called.

The callbacks to the boost::asio::async* functions are called whenever, cancel or close is called on the relevant timer or socket. Normally you would just make the appropriate cancel/close calls in the destructor using Stroustrup's beloved RAII pattern; job done.

However, the destructor won't be called when the owner deletes the object, because the callbacks still hold copies of the shared_ptrs and so their use_count will be greater than zero, resulting in a resource leak. The leak can be avoided by making the appropriate cancel/close calls prior to deleting the object. But it’s not as fool-proof as using RAII and making the cancel/close calls in the destructor. Ensuring that the resources are always freed, even in the presence of exceptions.

An RAII conforming pattern is to use static functions for callbacks and pass a weak_ptr to boost::bind when registering the callback function as in the example below:

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

Note: the read_buffer_ is stored as a shared_ptr in the Connection class and passed to the read_callback function as a shared_ptr.

This is to ensure that where multiple io_services are run in separate tasks, the read_buffer_ is not deleted until after the other tasks have completed, i.e. when the read_callback function has been called.

like image 35
kenba Avatar answered Sep 21 '22 16:09

kenba