0

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:

  1. 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?

  2. If 1) is correct, why does the server/chat-multi example provided above not explicitly wrap the async_read work on a strand?

  3. 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

  • Q1. is obviously correct, as *you* control where you call a function. In this case you call `async_receive` and it will be run wherever you invoke it. If it is on the strand, fine. If not, you might want to post/dispatch/defer to it. – sehe Dec 09 '21 at 12:48

1 Answers1

2

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):

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thanks for your response - this is starting to make a lot more sense. So to clarify: sockets get a copy of the strand that they are created on/with, and the get_executor() method returns that same executor. If the Asio user ensures that member function invocations on the socket object happen on the same strand as that returned by `get_executor()` then all is good. My final bit of confusion is that in the linked question, make_strand is also used when constructing the socket, yet an explicit dispatch was done with `stream_.get_executor()` - is this unnecessary in the docs? – FlyingOmelette Dec 09 '21 at 17:19
  • Yes. One precision: "sockets get a copy of the strand" - they get a copy of the strand _executor_. Executors are cheap references, the strand itself "lives" inside an Execution Context (of which `io_context` and `thread_pool` are common examples). – sehe Dec 09 '21 at 17:50
  • Regarding the other question, what linked post is it referring to exactly? – sehe Dec 09 '21 at 17:50
  • Thanks - executors are cheap references to a strand I'm assuming? The question in the comment was in reference to [this linked post](https://stackoverflow.com/questions/68779959/why-is-netdispatch-needed-when-the-i-o-object-already-has-an-executor). In that Boost.Beast example, `make_strand` is used when calling `async_accept` on the acceptor, yet an explicit dispatch was done as a subsequent call after creating the socket. – FlyingOmelette Dec 09 '21 at 18:17
  • Heh. I recognize [that answer](https://stackoverflow.com/a/68782989/85371) :) In the sample from the question/docs the purpose is to make sure that any initiation function(s) invoked from `on_run` will be on the strand. Incidentally, as long as you know that nothing else can possibly hold a reference to the shared resources (that are supposed to be protected from concurrent unsequenced access) then you can actually just invoke directly, as [the last paragraph of my answer](https://stackoverflow.com/questions/68779959/bla#:~:text=Arguably%2C%20at%20the,to%20it%20yet) tried to convey. – sehe Dec 09 '21 at 19:18
  • Haha it's a good answer, it just went straight over my head. Thanks for your help, really appreciated! – FlyingOmelette Dec 09 '21 at 22:52