0

I am trying to use boost ASIO with c++20 coroutines, but I'm struggling to find examples and documentation.

Currently, I have several objects, each owning a strand. They can post tasks to each other, the tasks execute in the strand owned by the callee, then they return to the caller by posting a continuation lambda (bound with bind_executor) and execution continues on the strand of the caller. This works quite well.

I'd like to replace the continuations with coroutines and co_await calls to make things more readable, so here's what I thought would do that:

boost::asio::awaitable<int> Object1::getValue()
{
    // Ensure we're on the local strand
    if (!m_Object1Strand.running_in_this_thread())
    {
        co_await boost::asio::post(m_Object1Strand, boost::asio::use_awaitable);
    }

    // Make some call to another object that has its own strand
    auto value = co_await m_Object2.readValue();
    
    // Combine the result with some local state
    auto result = value * m_Member;
    
    co_return result;
}


boost::asio::awaitable<int> Object2::readValue()
{
    // Switch to Object2's local strand
    if (!m_Object2Strand.running_in_this_thread())
    {
        co_await boost::asio::post(m_Object2Strand, boost::asio::use_awaitable);
    }

    // Do some async work, return a value
    co_return m_SomeLocalState;
}

This looks pretty reasonable to me, but it doesn't do what I expect.

The call to m_Object2.readValue() contains a switch to Object2's strand (since it needs to access the state in that object). Once we've returned to Object1 it seems rather random which strand we end up on. I expected/hoped that ASIO would be smart enough to transparently return me to the strand I originally awaited from, but it seems that is not the case. The access to m_Member there would be unsafe if we aren't on the right strand.

I figured it may be possible to inherit from boost::asio::awaitable and make it switch back to the caller's strand somehow, but I've completely failed in figuring out how to do that and I'm wondering if maybe I'm completely off the mark and there is a different pattern I'm supposed to be using.

Blastfurnace
  • 18,411
  • 56
  • 55
  • 70
coko
  • 1

1 Answers1

0

Logically, a coroutine is always a "logical strand" in that no two steps ever coincide or overlap. That's by definition how coroutines are resumed.

If you want the coroutine to execute on a specific strand to synchronize with other actors that it shares resources with, you can co_spawn it directly to that strand.

Active Object Pattern

That said it looks like you want your types to be Active Objects, and the post that you include seems reasonable enough. However, as I explained in other places¹ associated executors trump the one specified in the post. So you should write

// Ensure we're on the local strand
if (!m_Object1Strand.running_in_this_thread())
{
    co_await boost::asio::post(m_Object1Strand, boost::asio::use_awaitable);
}

As

co_await dispatch(bind_executor(m_Object1Strand, asio::use_awaitable));

It also looks like you really have to make sure you have them ordered correctly. So not like:

auto token = bind_executor(m_Object1Strand, asio::use_awaitable);
co_await dispatch(token);
auto value = co_await m_Object2.readValue(); // might resume on another executor
co_return value * m_Member;

But like

auto value = co_await m_Object2.readValue(); // might resume on another executor
co_await dispatch(token); // guard the member access
co_return value * m_Member;

¹ see

sehe
  • 374,641
  • 47
  • 450
  • 633
  • Thanks sehe. Good point about using dispatch, that did occur to me actually. The problem is that I may have several `co_await`s intermixed with accesses to the local state of the object. If each `co_await` can resume on a random strand I'll need to remember to do the `dispatch(token)` after every single one, which can be error-prone. Ideally, I want be able to say "everything within this method must always return to the strand it left off from". But I suspect that's impossible. – coko Apr 25 '23 at 16:43
  • As a separate thought, perhaps you know of an idiomatic way of using `bind_executor` with `co_await`? That would be a good enough solution for me. If I could write `auto value = co_await bind_executor(m_Object1Strand, m_Object2.readValue(use_awaitable));` for instance. – coko Apr 25 '23 at 16:47