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.