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 CompletionToken
s 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
.
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.
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:
#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.