Reputation: 3108
I'm writing a simple tcp socket server capable of handling multiple concurrent connections. The idea is that the main listening thread will do a blocking accept and offload socket handles to a worker thread (in a thread pool) to handle the communication asynchronously from there.
void server::run() {
{
io_service::work work(io_service);
for (std::size_t i = 0; i < pool_size; i++)
thread_pool.push_back(std::thread([&] { io_service.run(); }));
boost::asio::io_service listener;
boost::asio::ip::tcp::acceptor acceptor(listener, ip::tcp::endpoint(ip::tcp::v4(), port));
while (listening) {
boost::asio::ip::tcp::socket socket(listener);
acceptor.accept(socket);
io_service.post([&] {callback(std::move(socket));});
}
}
for (ThreadPool::iterator it = thread_pool.begin(); it != thread_pool.end(); it++)
it->join();
}
I'm creating socket
on the stack because I don't want to have to repeatedly allocate memory inside the while(listening)
loop.
The callback function callback
has the following prototype:
void callback(boost::asio::socket socket);
It is my understanding that calling callback(std::move(socket))
will transfer ownership of socket
to callback
. However when I attempt to call socket.receive()
from inside callback
, I get a Bad file descriptor
error, so I assume something is wrong here.
How can I transfer ownership of socket
to the callback function, ideally without having to create sockets on the heap?
Upvotes: 2
Views: 1021
Reputation: 51891
Undefined behavior is potentially being invoked, as the lambda may be invoking std::move()
on a previously destroyed socket via a dangling reference. For example, consider the case where the loop containing the socket ends its current iteration, causing socket
to be destroyed, before the lambda is invoked:
Main Thread | Thread Pool
-----------------------------------+----------------------------------
tcp::socket socket(...); |
acceptor.accept(socket); |
io_service.post([&socket] {...}); |
~socket(); // end iteration |
... // next iteration | callback(std::move(socket));
To resolve this, one needs to transfer socket
ownership to the handler rather than transfer ownership within the handler. Per documentation, Handlers must be CopyConstructible
, and hence their arguments, including the non-copyable socket
, must be as well. Yet, this requirement can be relaxed if Asio can eliminate all calls to the handler's copy constructor and one has defined BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
.
#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
#include <boost/asio.hpp>
void callback(boost::asio::ip::tcp::socket socket);
...
// Transfer ownership of socket to the handler.
io_service.post(
[socket=std::move(socket)]() mutable
{
// Transfer ownership of socket to `callback`.
callback(std::move(socket));
});
For more details on Asio's type checking, see this answer.
Here is a complete example demonstrating a socket
's ownership being transferred to a handler:
#include <functional> // std::bind
#include <utility> // std::move
#include <vector> // std::vector
#define BOOST_ASIO_DISABLE_HANDLER_TYPE_REQUIREMENTS
#include <boost/asio.hpp>
const auto noop = std::bind([]{});
void callback(boost::asio::ip::tcp::socket socket)
{
const std::string actual_message = "hello";
boost::asio::write(socket, boost::asio::buffer(actual_message));
}
int main()
{
using boost::asio::ip::tcp;
// Create all I/O objects.
boost::asio::io_service io_service;
tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 0));
tcp::socket client_socket(io_service);
// Connect the sockets.
client_socket.async_connect(acceptor.local_endpoint(), noop);
{
tcp::socket socket(io_service);
acceptor.accept(socket);
// Transfer ownership of socket to the handler.
assert(socket.is_open());
io_service.post(
[socket=std::move(socket)]() mutable
{
// Transfer ownership of socket to `callback`.
callback(std::move(socket));
});
assert(!socket.is_open());
} // ~socket
io_service.run();
// At this point, sockets have been conencted, and `callback`
// should have written data to `client_socket`.
std::vector<char> buffer(client_socket.available());
boost::asio::read(client_socket, boost::asio::buffer(buffer));
// Verify the correct message was read.
const std::string expected_message = "hello";
assert(std::equal(
begin(buffer), end(buffer),
begin(expected_message), end(expected_message)));
}
Upvotes: 2