1

I am working on a boost asio project, where I have a bunch of stackful coroutines (https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/overview/composition/spawn.html), performing asynchronous operations, and I use a strand to synchronize access to critical sections like so:

io_context ioctx;
io_context::strand my_strand{ioctx};

void my_coroutine(yield_context yield) {
  while (true) {
    post(my_strand, yield);
    // Critical section, where one coroutine is supposed to have access
    post(ioctx, yield);
  }
}

However, I began to suspect that mutual exclusion was not happening, and wrote a small test to test out my hypothesis:

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

using namespace boost::asio;

io_context ioctx;
io_context::strand my_strand{ioctx};

std::atomic_int cnt{0};

void my_coroutine(yield_context yield) {
  while (true) {
    post(my_strand, yield);
    cnt++;
    assert(cnt == 1);
    assert(my_strand.running_in_this_thread());
    cnt--;
    post(ioctx, yield);
  }
}

int main() {
  spawn(ioctx, [&](yield_context yield) { my_coroutine(yield); });
  spawn(ioctx, [&](yield_context yield) { my_coroutine(yield); });

  for (int i = 0; i < 5; ++i) {
    std::thread([] { ioctx.run(); }).detach();
  }
  getchar();
}

As I suspected, both assertions fail when the program runs. I am not sure where the issue is. Most surprisingly, commenting out one of the spawn calls still causes the running_in_this_thread assertion to fail!

I am using Boost v1.81.0 on linux.

Suraaj K S
  • 600
  • 3
  • 21

1 Answers1

1

The executor specified in post is just a fallback. The associated executor of the token/handler takes precedence. In your case, the associated executor is the coroutine's strand. Your attempt to switch executors does nothing. To force your choice of executor, you can make sure it is associated with the token:

post(bind_executor(my_strand, yield));

and

post(bind_executor(ioctx, yield));
sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thanks for your answer. It does seem to work. However, I'm looking at the boost documentation here: https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/post/overload2.html, which says `this function submits an object for execution using the specified executor`. It does not talk about precedence at all. Am I missing something? – Suraaj K S Jul 29 '23 at 20:55
  • 1
    It's an overriding principle. It's how any handler gets invoked. So, it might actually _submit_ using the executor [making the wording okay], but the bound executor will dictate where it executes. I guess the docs here could shed more light https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/overview/model/associators.html, and the corresponding deprecated interface https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/asio_handler_invoke.html – sehe Jul 30 '23 at 00:50
  • 1
    Oh, wow, the page you linked https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/post/overload2.html contains the whole implementation specification in excruciating detail. Including the `auto ex1 = get_associated_executor(handler, ex);` step – sehe Jul 30 '23 at 00:53
  • Ah I see. Thanks for clarifying! So, if `yield`'s associated executor is an implicit strand with an underlying ioctx executor (as is the case in my example), I can just do `post(yield)` instead of `post(bind_executor(ioctx, yield))`? I suspect that `bind_executor` doesn't actually change `yield`'s state (associated_executor). – Suraaj K S Jul 30 '23 at 01:12
  • Bind_executor will certainly change the associated executor (which is a _decorator_ of the handler, not mutation). It might actually result in an [_equivalent executor_](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/Executor1.html#boost_asio.reference.Executor1.standard_executors:~:text=x1%20and%20x2-,can%20be%20interchanged%20with%20identical%20effects,-in%20any%20of) executor being associated. With respect to `post(yield)` being identical to `post(get_associated_executor(yield), yield)`, yes this is true by definition. – sehe Jul 30 '23 at 01:36
  • Do keep in mind though that it seems unwise to **depend** on there being an explicit associated executor unless it is documented. If the implementation changes in a way to remove that characteristic, you will end up posting to the fallback, which is `system_executor()` which is almost certainly not what you want. I have some earlier answer going into more detail with examples, e.g. https://stackoverflow.com/a/66733239/85371 – sehe Jul 30 '23 at 01:44
  • I suppose it _is documented_: https://www.boost.org/doc/libs/master/doc/html/boost_asio/reference/basic_yield_context/get_executor.html which satisfies the default implementation of [get_associated_executor](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/reference/get_associated_executor/overload1.html) via the [primary template](https://www.boost.org/doc/libs/1_82_0/doc/html/boost_asio/overview/model/associators.html#:~:text=The%20trait%27s%20primary%20template%20is%20specified%20such%20that) – sehe Jul 30 '23 at 01:50
  • Thanks for your comments. However, I was curious about the equivalence of `post(yield)` and `post(bind_executor(ioctx, yield))` in my example, and not `post(yield)` and `post(get_associated_executor(yield), yield)`, which is always true. However, as you mention, `bind_executor` decorates, and does not mutate, and so I am guessing `post(yield)` and `post(bind_executor(ioctx, yield))` do the same thing in my example. – Suraaj K S Jul 30 '23 at 15:51
  • I agree with the assessment (assuming the question code). I suppose you should be able to check for equivalence like I mentioned before. – sehe Jul 30 '23 at 21:50