Search code examples
c++boost-asioboost-beast

Why is `net::dispatch` needed when the I/O object already has an executor?


I'm learning Boost.Beast & Boost.Asio from this example libs/beast/example/http/server/async-ssl/http_server_async_ssl.cpp - 1.77.0.

As far as I know, all I/O operations that happen on a I/O object happen in the I/O execution context of the object. The asynchronous operations will be in the same thread as the run of the I/O context, because they are all called (indirectly) by run of the I/O context.

In this example (see above link please), when the connection is established, the acceptor assigns a dedicated strand to the new connection:

    do_accept()
    {
        // The new connection gets its own strand
        acceptor_.async_accept(
            net::make_strand(ioc_),
            beast::bind_front_handler(
                &listener::on_accept,
                shared_from_this()));
    }

Does it imply that all I/O operations that happen on the new connections happen in the strand? If so, why does the example use net::dispatch to specify the strand AGAIN when it is going to call async_read?

    // 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()));
    }

What is the difference if we just call the async_read directly without going through net::dispatch? Thanks. :)


Solution

  • Does it imply that all I/O operations that happen on the new connections happen in the strand?

    It means that the accepted socket gets a copy of the executor that refers to the strand. This does mean that all async operations initiated from that service object will by default (!) invoke their completion handlers on that strand. However, this does not apply to any other operations, like the initiation function (e.g. s.async_read_some) itself.

    So, to make sure all operations happen on the strand, you have to make sure that any initiations happen on the same strand. In many cases, this would be automatic because many initiations happen in completions for the previous operation - thus already on the strand - but not for the first one, as you can see.

    (Arguably, at the start of a new strand, so when an IO object has just been created and the strand is private to that IO object, the very first initiation could be safely made from any thread, logically becoming part in the strand. You could view it as the forking-point ("birth") of the strand itself. That's because no operations can be in flight and no other thread can possibly hold a reference to it yet.)