17

I am trying to wrap my head around resource management in boost::asio. I am seeing callbacks called after the corresponding sockets are already destroyed. A good example of this is in the boost::asio official example: http://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/example/cpp11/chat/chat_client.cpp

I am particularly concerned with the close method:

void close()
{
   io_service_.post([this]() { socket_.close(); });
}

If you call this function and afterwards destruct chat_client instance that holds socket_, socket_ will be destructed before the close method is called on it. Also any pending async_* callbacks can be called after the chat_client has been destroyed.

How would you correctly handle this?

roalz
  • 2,699
  • 3
  • 25
  • 42
Julian Stecklina
  • 1,271
  • 1
  • 10
  • 24
  • To your [now-deleted comment](http://stackoverflow.com/questions/35393015/how-do-you-correctly-close-sockets-in-boostasio#comment58492034_35393083) _"Closing sockets does definitely not cause all pending callbacks to be called with an error."_: how would you word it then? – sehe Feb 14 '16 at 20:57

2 Answers2

18

You can do socket_.close(); almost any time you want, but you should keep in mind some moments:

  • If you have threads, this call should be wrapped with strand or you can crash. See boost strand documentation.
  • Whenever you do close keep in mind that io_service can already have queued handlers. And they will be called anyway with old state/error code.
  • close can throw an exception.
  • close does NOT includes ip::tcp::socket destruction. It just closes the system socket.
  • You must manage object lifetime yourself to ensure objects will be destroyed only when there is no more handlers. Usually this is done with enable_shared_from_this on your Connection or socket object.
Julian Stecklina
  • 1,271
  • 1
  • 10
  • 24
Galimov Albert
  • 7,269
  • 1
  • 24
  • 50
  • 36
    It's too complicated, ordinary mortal programmers should avoid closing the boost asio sockets, better not open them at all. – sqr163 Apr 06 '17 at 18:32
  • "If you have threads, this call should be wrapped with strand or you can crash." - Not true, you can use `close()` from one thread while making other calls (read/write/connect) from another. What makes you think you can't? You do need to be careful to only call `close()` once though. – Arthur Tacca Jul 14 '23 at 10:28
  • @ArthurTacca `man close` makes me think that it's not a good idea to call with other calls concurrently – Galimov Albert Jul 25 '23 at 17:55
11

Invoking socket.close() does not destroy the socket. However, the application may need to manage the lifetime of objects for which the operation and completion handlers depend upon, but this is not necessarily the socket object itself. For instance, consider a client class that holds a buffer, a socket, and has a single outstanding read operation with a completion handler of client::handle_read(). One can close() and explicitly destroy the socket, but the buffer and client instance must remain valid until at least the handler is invoked:

class client
{
  ...

  void read()
  {
    // Post handler that will start a read operation.
    io_service_.post([this]() {
      async_read(*socket, boost::asio::buffer(buffer_);
        boost::bind(&client::handle_read, this,
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred));
    });
  }

  void handle_read(
    const boost::system::error_code& error,
    std::size_t bytes_transferred
  )
  {
    // make use of data members...if socket_ is not used, then it
    // is safe for socket to have already been destroyed.
  }

  void close()
  {
    io_service_.post([this]() {
      socket_->close();
      // As long as outstanding completion handlers do not
      // invoke operations on socket_, then socket_ can be 
      // destroyed.
      socket_.release(nullptr);
    });
  }

private:
  boost::asio::io_service& io_service_;

  // Not a typical pattern, but used to exemplify that outstanding
  // operations on `socket_` are not explicitly dependent on the 
  // lifetime of `socket_`.
  std::unique_ptr<boost::asio::socket> socket_;
  std::array<char, 512> buffer_;
  ...
}

The application is responsible for managing the lifetime of objects upon which the operation and handlers are dependent. The chat client example accomplishes this by guaranteeing that the chat_client instance is destroyed after it is no longer in use, by waiting for the io_service.run() to return within the thread join():

int main(...)
{
  try
  {
    ...

    boost::asio::io_service io_service;
    chat_client c(...);

    std::thread t([&io_service](){ io_service.run(); });

    ...

    c.close();
    t.join(); // Wait for `io_service.run` to return, guaranteeing
              // that `chat_client` is no longer in use.
  }           // The `chat_client` instance is destroyed.
  catch (std::exception& e)
  {
    ...
  }
}

One common idiom is to managing object lifetime is to have the I/O object be managed by a single class that inherits from enable_shared_from_this<>. When a class inherits from enable_shared_from_this, it provides a shared_from_this() member function that returns a valid shared_ptr instance managing this. A copy of the shared_ptr is passed to completion handlers, such as a capture-list in lambdas or passed as the instance handle to bind(), causing the lifetime of the I/O object to be extended to at least as long as the handler. See the Boost.Asio asynchronous TCP daytime server tutorial for an example using this approach.

Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • In the `enable_shred_from_this<>` [example](https://www.boost.org/doc/libs/1_60_0/doc/html/boost_asio/tutorial/tutdaytime3/src.html) they don't explicitly call the `socket_.close()` method. Is it safe to assume that the socket close is handled automatically? – Tonia Sanzo Nov 24 '21 at 18:35