c++c++11asynchronousboostboost-asio

When must you pass io_context to boost::asio::spawn? (C++)


I was surprised to find that the code below works without passing io_context as the first argument to spawn. Could somebody please explain why I don't need to pass it in this case, and in what situations you must explicitly pass it. I'm using Boost 1.75.0.

#include <boost/asio/spawn.hpp>
#include <boost/asio/deadline_timer.hpp>
#include <iostream>

int main() {

  boost::asio::io_context io_context;
  boost::asio::deadline_timer timer(io_context);

  boost::asio::spawn([&](boost::asio::yield_context yield){ // don't need to pass io_context?!
    std::cout << "started spawn" << std::endl;
    timer.expires_from_now(boost::posix_time::seconds(5));
    timer.async_wait(yield);
    std::cout << "finished spawn" << std::endl;
  });

  std::cout << "running io_context" << std::endl;
  io_context.run();
  std::cout << "finished running io_context" << std::endl;

}

Solution

  • Asio has added the concept of associated executors and default executors.

    The associated executors is not really new, because the handler_invoke protocol already allowed for handler-type specific semantics. However, since the formulation of the executor concept it became more generalized.

    Now you can post any handler, and it will execute on the associated executor, the executor supplied or the default executor. The default executor is ultimately system_executor{}.

    Boost 1.90.0 changed the default associated executor from system_executor to inline_executor: "Changed the default candidate executor for associated_executor from system_executor to inline_executor"

    So

    post([]{ puts("Hello world"); });
    post(system_executor{}, []{ puts("Hello world"); });
    

    Both invoke the handler using system_executor.

    You can bind an associated handler with any handler that doesn't associate one already:

    post(bind_executor(ex1, []{ puts("Hello world"); }));
    post(system_executor{}, bind_executor(ex1, []{ puts("Hello world"); }));
    

    Both run the handler on ex1, not the fallbacks. Combining the above, you will already expect that this does the same:

    post(ex1, []{ puts("Hello world"); });
    

    (here, the handler has not associated executor, so ex1 functions as the fallback)

    Spawn

    Spawn is merely a wrapper that "posts" another type of handler¹. Indeed it is documented to use any associated executor. The implementation reflects this quite readably:

    template <typename Function>
    inline void spawn(BOOST_ASIO_MOVE_ARG(Function) function,
        const boost::coroutines::attributes& attributes)
    {
      typedef typename decay<Function>::type function_type;
    
      typename associated_executor<function_type>::type ex(
          (get_associated_executor)(function));
    
      boost::asio::spawn(ex, BOOST_ASIO_MOVE_CAST(Function)(function), attributes);
    }
    

    You can see that get_associated_executor is called without explicit fallback, defaulting to system_executor again.

    Side Notes

    In addition


    ¹ It's an implementation detail, but it will generally be boost::asio::detail::spawn_helper<...> which correctly propagates associated executors/allocators again. I would dub this kind of type a "handler binder"

    LIVE DEMO

    To illustrate the reality that system_executor is being used, here's a simplified tester:

    Compiler Explorer

    #include <boost/asio/spawn.hpp>
    #include <boost/asio/steady_timer.hpp>
    #include <iostream>
    
    int main() {
        using namespace boost::asio;
        using namespace std::chrono_literals;
        io_context ctx(1);
    
        spawn([](yield_context yield) {
            std::cout << "started spawn" << std::endl;
    
            auto ex = get_associated_executor(yield);
            //auto work = make_work_guard(ex);
    
            steady_timer timer(ex, 5s);
            timer.async_wait(yield);
    
            std::cout << "finished spawn" << std::endl;
        });
    
        std::cout << "running context" << std::endl;
        query(system_executor{}, execution::context).join();
        std::cout << "finished running context" << std::endl;
    }
    

    Notes:

    Now it prints

    started spawn
    running context
    finished spawn
    finished running context
    

    with the 5s delay as expected.