Search code examples
c++boostboost-asio

Boost Asio: What's the difference between the Executor passed to boost::asio::post and the associated executor of the CompletionToken?


For methods like post, and dispatch there is one overload taking only a CompletionToken and another additionally taking an Executor. As far as I know, the overload without Executor works as if the overload with the Executor was called with the CompletionTokens associated executor. However, what is the effect of passing an Executor to post that is not the associated executor of the CompletionToken? What's the role of each of the executors? What executor will the CompetionToken's handler be executed on?

I already found out that the assertion in

boost::asio::io_context ioc;
auto ioc_ex = ioc.get_executor();

auto strand_a = boost::asio::make_strand(ioc_ex);
auto strand_b = boost::asio::make_strand(ioc_ex);

boost::asio::post(strand_a, boost::asio::bind_executor(strand_b, [&]() {
    assert(strand_a.running_in_this_thread()
        && strand_b.running_in_this_thread());
}));
ioc.run();

succeeds. This surprises me since I assumed that a CompetionToken's handler would only ever be executed in a single Executor (strand in this case).

In other answers (e.g. here and here) I've read the Executor passed to post works as a fallback Executor for execution of the CompletionToken's handler, in case the latter does not have an associated executor. However, this cannot be its only use, since otherwise, the code above would simply ignore the "fallback" Executor stand_a, given the CompletionToken is explicitly bound to stand_b.


Solution

  • The handler is always invoked on the associated executor.

    An explicitly specified executor argument is only required for the case where there is no associated executor and the default (system_executor) is not desired.

    But What About The Test Case?

    Your example is a mind-bender. I'm afraid it simply comes down to the implementation details that are unspecified. We can only observe partial observations "from the outside" and the fact that a.running_in_this_thread() returns true doesn't mean that the handler was intentionally invoked in that context.

    I slightly expanded your example to drive home this point:

    Live On Coliru

    #include <boost/asio.hpp>
    namespace asio = boost::asio;
    
    int main() {
        asio::io_context ioc;
    
        auto a = make_strand(ioc);
        auto b = make_strand(ioc);
    
        post(bind_executor(b, [&]() {
            assert(                               //
                not a.running_in_this_thread() && //
                b.running_in_this_thread());
        }));
        post(a, bind_executor(b, [&]() {
                 assert(                           //
                     a.running_in_this_thread() && //
                     b.running_in_this_thread());
             }));
        post(bind_executor(b, [&]() {
            assert(                               //
                not a.running_in_this_thread() && //
                b.running_in_this_thread());
        }));
    
        ioc.run();
    }
    

    Prints, e.g.:

    sotest: /home/sehe/Projects/stackoverflow/test.cpp:18: auto main()::(anonymous class)::operator()() const: Assertion
     `a.running_in_this_thread() && b.running_in_this_thread()' failed.
    

    Note how the change doesn't even happen in the third handler posted! The mere presence of the third post changes (unspecified aspects of) the effective context in which of the second handler runs.

    This is of course, fine, as long as the documented guarantees are met.