c++boost-asioboost-beast

When do I need to dispatch to strand with Boost Beast?


I have this code from Boost Beast async chat server examples:

void ChatServer::StartAccept() {
    acceptor.async_accept(
        boost::asio::make_strand(acceptor.get_executor()), 
        [self = shared_from_this()] (error_code err, tcp::socket socket) {
            if (err) return self->Fail(err, "Failed to accept connection");

            std::make_shared<HttpSession>(std::move(socket), self->sslContext,  self->room)->Start();

            self->StartAccept();
        });
}

Now I understand that I have to make a separate strand for each connection/socket, because I'm running one io_context in multiple threads. If I don't do that, I could have threads reading/writing from the same socket which is a data race.

However in the example they still use dispatch to the executor associated with the socket, which is strand?

void HttpSession::Start() {
    boost::asio::dispatch(
        stream.get_executor(),
        [self = shared_from_this()]() {
            self->DoSslHandshake();
        });
}

Why do I have to use dispatch here? Isn't every socket already associated with its own strand?


Solution

  • It's not required, but that's only coincidental. The code expresses intent.

    Re: > Why do I have to use dispatch here? Isn't every socket already associated with its own strand?

    That's irrelevant. The IO object's executor will only serve as the default for unbound completion tokens. However, what the dispatch is guarding is initiation not (intermediate) handler invocation.

    In fact, many of the library examples have an explanatory comment:

    // Start the asynchronous operation
    void
    run()
    {
        // We need to be executing within a strand to perform async operations
        // on the I/O objects in this session. Although not strictly necessary
        // for single-threaded contexts, this example code is written to be
        // thread-safe by default.
        net::dispatch(
            stream_.get_executor(),
            beast::bind_front_handler(
                &session::on_run,
                shared_from_this()));
    }
    

    I've elaborated on this before more relevant to your context:

    void TCPSession::start() {
      // Here posting to the strand is probably redundant as a typical listener
      // will immediaitely invoke `start()` on a newly constructed session
      // instance, so by definition no other thread(s) can hold a reference to it.
      // However in the interest of showing the principles, lets post to IO
      // service:
      post(m_socket.get_executor(),
           [this, self = shared_from_this()] { do_read_header(); });
    }
    

    Or, similary, but way more brief:

    void Connection::start() { // always assumed on the strand (since connection
                                // just constructed)
         do_read();
     }