Home > Back-end >  Boost.Asio - when is explicit strand wrapping needed when using make_strand
Boost.Asio - when is explicit strand wrapping needed when using make_strand

Time:12-10

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

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

  • Related