c++boost-asioboost-coroutine

Can I spawn multiple coroutines to the same strand without them overlapping?


I'm trying to invoke boost::asio::spawn twice to the same boost::asio::io_context::strand, passing a coroutine each time and I was expecting the two coroutines to execute one after the other but they are instead executing in parallel. Here's code that illustrates this:

boost::asio::io_context ioc;
boost::asio::io_context::strand strand{ioc};
boost::asio::spawn(strand, [&](boost::asio::yield_context yield)
                   {
                       cout << "1\n";
                       ioc.post(yield);
                       cout << "2\n";
                       ioc.post(yield);
                       cout << "3\n";
                   });

boost::asio::spawn(strand, [&](boost::asio::yield_context yield)
                   {
                       cout << "10\n";
                       ioc.post(yield);
                       cout << "20\n";
                       ioc.post(yield);
                       cout << "30\n";
                   });
ioc.run();

This outputs:

1
10
2
20
3
30

When I was expecting:

1
2
3
10
20
30

In real code, the first coroutine sets up a socket (going through the motions of resolving/connecting/handshaking) and the second does a send/receive. My intent was to "append" the second coroutine to the strand and have it start executing only when the first one is done.

How can I achieve this effect?

Edit: More context. The first coroutine is in a constructor, the second in a member function. If I want to allow the user to write

Foo foo;
foo.bar();

how could I ensure the coroutine inside the constructor is finished before the one in bar() starts?


Solution

  • strands only guarantee that their functions won't be executed on multiple threads at the same time. This allows you to not need to use locks.

    They do not make separate functions execute sequentially. If you want sequential execution simply call your second function at the end of your first function:

    boost::asio::io_context ioc;
    boost::asio::io_context::strand strand{ioc};
    auto main = [&](boost::asio::yield_context yield)
                       {
                           cout << "10\n";
                           ioc.post(yield);
                           cout << "20\n";
                           ioc.post(yield);
                           cout << "30\n";
                       };
    boost::asio::spawn(strand, [&](boost::asio::yield_context yield)
                       {
                           cout << "1\n";
                           ioc.post(yield);
                           cout << "2\n";
                           ioc.post(yield);
                           cout << "3\n";
                           main();
                       });
    

    If you can't call the second function from the first a technique I have used a few times is to have a queue of functions to be executed, as everything is in a strand no need to worry about locking:

    bool executing = false;
    struct ExecuteLock
    {
      ExecuteLock()
      {
        if ( !executing )
        {
          executing = true;
          locked = true;
        }
        else
        {
          locked = false;
        }
      }
    
      ~ExecuteLock()
      {
        if ( locked )
        {
          executing = false;
        }
      }
    
      bool locked;
    };
    
    typedef QueueFunction std::function<void(boost::asio::yield_context yield);
    
    std::queue< QueueFunction > executeQueue;
    
    void run( QueueFunction f )
    {
      boost::asio::spawn( strand, [=](boost::asio::yield_context yield)
      {
        ExecuteLock lock;
        if (!lock.locked)
        {
          executeQueue.push( f );
          return;
        }
        f();
        while ( !executeQueue.empty() )
        {
          executeQueue.front()();
          executeQueue.pop();
        }
      } );
    }
    

    You can then just call run() each time you want to execute something.