Reputation: 1281
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?
Upvotes: 17
Views: 27629
Reputation: 7357
You can do socket_.close();
almost any time you want, but you should keep in mind some moments:
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.Connection
or socket
object.Upvotes: 17
Reputation: 51961
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.
Upvotes: 11