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

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{}.

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

  • spawn will add a strand where appropriate (this is a reason why providing a concurrency hint when constructing your execution context can make a big difference)
  • spawn can take a yield_context as the first argument, in which case you will effectively run on the same strand (sharing the executor)

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

  • ctx now takes a concurrency hint (as mentioned above)

  • ctx is never used; joining it would not wait for coro to be completed!

  • Note the commented work. It is important that though async operations constitute work, the Coro itself is not work so you might want to guard the scope of the coro in some situations.

  • Note that system_executor is joined like you would another thread-based execution context like thread_pool:

     query(system_executor{}, execution::context).join();
    

Now it prints

started spawn
running context
finished spawn
finished running context

with the 5s delay as expected.

Leave a Comment