I have been researching Boost.Asio and Boost.Beast and have some confusion around when explicit strand wrapping is needed with socket::async_*
member function calls.
In Boost.Asio (1.78), there is a make_strand function. The examples provided with Boost.Beast show it being used like this:
server/chat-multi/listener.cpp
void
listener::
run()
{
// The new connection gets its own strand
acceptor_.async_accept(
net::make_strand(ioc_),
beast::bind_front_handler(
&listener::on_accept,
shared_from_this()));
}
//...
// Handle a connection
void
listener::
on_accept(beast::error_code ec, tcp::socket socket)
{
if(ec)
return fail(ec, "accept");
else
// Launch a new session for this connection
boost::make_shared<http_session>(std::move(socket), state_)->run();
// The new connection gets its own strand
acceptor_.async_accept(
net::make_strand(ioc_),
beast::bind_front_handler(
&listener::on_accept,
shared_from_this()));
}
server/chat-multi/http_session.cpp
void
http_session::
run()
{
do_read();
}
//...
void
http_session::
do_read()
{
// Construct a new parser for each message
parser_.emplace();
// Apply a reasonable limit to the allowed size
// of the body in bytes to prevent abuse.
parser_->body_limit(10000);
// Set the timeout.
stream_.expires_after(std::chrono::seconds(30));
// Read a request
http::async_read(
stream_,
buffer_,
parser_->get(),
beast::bind_front_handler(
&http_session::on_read,
shared_from_this()));
}
void
http_session::
on_read(beast::error_code ec, std::size_t)
{
// This means they closed the connection
if(ec == http::error::end_of_stream)
{
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
return;
}
// Handle the error, if any
if(ec)
return fail(ec, "read");
// See if it is a WebSocket Upgrade
if(websocket::is_upgrade(parser_->get()))
{
// Create a websocket session, transferring ownership
// of both the socket and the HTTP request.
boost::make_shared<websocket_session>(
stream_.release_socket(),
state_)->run(parser_->release());
return;
}
//...
}
server/chat-multi/websocket_session.cpp
void
websocket_session::
on_read(beast::error_code ec, std::size_t)
{
// Handle the error, if any
if(ec)
return fail(ec, "read");
// Send to all connections
state_->send(beast::buffers_to_string(buffer_.data()));
// Clear the buffer
buffer_.consume(buffer_.size());
// Read another message
ws_.async_read(
buffer_,
beast::bind_front_handler(
&websocket_session::on_read,
shared_from_this()));
}
In the same Boost.Beast example, subsequent calls on the socket's async_read
member function are done without explicitly wrapping the work in a strand, either via post
, dispatch
(with socket::get_executor
) or wrapping the completion handler with strand::wrap
.
Based on the answer to this question, it seems that the make_strand
function copies the executor into the socket object, and by default the socket object's completion handlers will be invoked on the same strand. Using socket::async_receive
as an example, this to me says that there are two bits of work to be done:
A) The socket::async_receive
I/O work itself
B) The work involved in calling the completion handler
My questions are:
According to the linked answer, when using
make_strand
B is guaranteed to be called on the same strand, but not A. Is this correct, or have I misunderstood something?If 1) is correct, why does the server/chat-multi example provided above not explicitly wrap the
async_read
work on a strand?In Michael Caisse's cppcon 2016 talk, "Asynchronous IO with Boost.Asio", he also does not explicitly wrap
async_read_until
operations in a strand. He explains that write calls should be synchronised with a strand, as they can in theory be called from any thread in the application. But read calls don't, as he is controlling them himself. How does this fit into the picture?
Thanks in advance
CodePudding user response:
If an executor is not specified or bound, the "associated executor" is used.
For member async initiation functions the default executor is the one from the IO object. In your case it would be the socket which has been created "on" (with) the strand executor. In other words, socket.get_executor()
already returns the strand<> executor.
Only when posting you would either need to specify the strand executor (or bind the handler to it, so it becomes the implicit default for the handler):