14

I am unable to form a mental picture of how the control flow happens with spawn.

  1. When I call spawn(io_service, my_coroutine), does it add a new handler to the io_service queue that wraps a call to the my_coroutine?

  2. When inside the coroutine I call an async function passing it my yield_context, does it suspend the coroutine until the async operation completes?

    void my_coroutine(yield_context yield)
    {
      ...
      async_foo(params ..., yield);
      ...   // control comes here only once the async_foo operation completes
    }

What I don't understand is how we avoid waits. Say if my_coroutine serves a TCP connection, how are other instances of my_coroutine invoked while on particular instance is suspended, waiting for async_foo to complete?

CppNoob
  • 2,322
  • 1
  • 24
  • 35

1 Answers1

39

In short:

  1. When spawn() is invoked, Boost.Asio performs some setup work and then will use a strand to dispatch() an internal handler that creates a coroutine using the user provided function as an entry point. Under certain conditions, the internal handler can be will be invoked within the call to spawn(), and other times it will be posted to the io_service for deferred invocation.
  2. The coroutine is suspended until either the operation completes and the completion handler is invoked, the io_service is destroyed, or Boost.Asio detects that the coroutine has been suspended with no way to resume it, at which point Boost.Asio will destroy the coroutine.

As mentioned above, when spawn() is invoked, Boost.Asio performs some setup work and then will use a strand to dispatch() an internal handler that creates a coroutine using the user provided function as an entry point. When the yield_context object is passed as a handler to asynchronous operations, Boost.Asio will yield immediately after initiating the asynchronous operation with a completion handler that will copy results and resume the coroutine. The previously mentioned strand is owned by the coroutine is used to guarantee the yield occurs before resume. Lets consider a simple example demonstrating spawn():

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

boost::asio::io_service io_service;

void other_work()
{
  std::cout << "Other work" << std::endl;
}

void my_work(boost::asio::yield_context yield_context)
{
  // Add more work to the io_service.
  io_service.post(&other_work);

  // Wait on a timer within the coroutine.
  boost::asio::deadline_timer timer(io_service);
  timer.expires_from_now(boost::posix_time::seconds(1));
  std::cout << "Start wait" << std::endl;
  timer.async_wait(yield_context);
  std::cout << "Woke up" << std::endl;    
}

int main ()
{
  boost::asio::spawn(io_service, &my_work);
  io_service.run();
}

The above example outputs:

Start wait
Other work
Woke up

Here is an attempt to illustrate the execution of the example. Paths in | indicate the active stack, : indicates the suspended stack, and arrows are used to indicate transfer of control:

boost::asio::io_service io_service;
boost::asio::spawn(io_service, &my_work);
`-- dispatch a coroutine creator
    into the io_service.
io_service.run();
|-- invoke the coroutine creator
|   handler.
|   |-- create and jump into
|   |   into coroutine         ----> my_work()
:   :                                |-- post &other_work onto
:   :                                |   the io_service
:   :                                |-- create timer
:   :                                |-- set timer expiration
:   :                                |-- cout << "Start wait" << endl;
:   :                                |-- timer.async_wait(yield)
:   :                                |   |-- create error_code on stack
:   :                                |   |-- initiate async_wait operation,
:   :                                |   |   passing in completion handler that
:   :                                |   |   will resume the coroutine
|   `-- return                 <---- |   |-- yield
|-- io_service has work (the         :   :
|   &other_work and async_wait)      :   :
|-- invoke other_work()              :   :
|   `-- cout << "Other work"         :   :
|       << endl;                     :   :
|-- io_service still has work        :   :
|   (the async_wait operation)       :   :
|   ...async wait completes...       :   :
|-- invoke completion handler        :   :
|   |-- copies error_code            :   :
|   |   provided by service          :   :
|   |   into the one on the          :   :
|   |   coroutine stack              :   :
|   |-- resume                 ----> |   `-- return error code
:   :                                |-- cout << "Woke up." << endl;
:   :                                |-- exiting my_work block, timer is 
:   :                                |   destroyed.
|   `-- return                 <---- `-- coroutine done, yielding
`-- no outstanding work in 
    io_service, return.
Tanner Sansbury
  • 51,153
  • 9
  • 112
  • 169
  • 1
    What does the act of copying `yield_context` as it is passed to a coroutine do? If `foo` passes `yield` to `bar`, `bar` passed to `baz`, and `baz` calls `yield` - does the control go directly back to `foo`? When the `timer.async_wait` call "yields", does the control go back to the coroutine-creator handler, which then returns? When the time later expires, how does the control go back to `async_wait` - which then returns to `my_work`? – CppNoob Jun 01 '15 at 03:06
  • @CppNoob Boost.Asio's first-class support for Boost.Coroutine is a fairly thin facade. Are you trying to understand how Boost.Asio uses Boost.Coroutine, or how Boost.Coroutine itself works? Copying `yield_context` just creates another `yield_context` (not a coroutine). When the `timer.async_wait()` yields the coroutine, control jumps to the left stack immediately after the point at which started the coroutine. When `async_wait`'s completion handler is invoked, it resumes the coroutine, causing execution to jump to the right stack immediately after the point where it had yielded. – Tanner Sansbury Jun 01 '15 at 03:31
  • 1
    I'm trying to understand how Boost Asio uses coroutines - not from an implementation angle but as a control flow mechanism. I think of the yield_context as a conduit for relinquishing control to another context. In this example, the yield_context in my_work refers to the coroutine-creator handler context, and it is copied as the completion handler for async_wait. But when the completion handler of async_wait executes, the control goes back to my_work, not the coroutine-creator handler (which has exited by then). I clearly don't understand this, and I hope I could describe what's not clear. – CppNoob Jun 01 '15 at 03:51
  • 1
    @CppNoob The `yield_context` is used to represent the currently executing stackful coroutine. In the above illustration, the coroutine is the stack on the right-side, and `yield_context` represents this within `my_work()`. Coroutines allow suspending and resuming execution at certain locations, preserving local state on each context switch. The coroutine suspends itself in `async_wait()`, and when the completion handler runs, it resumes the coroutine from the point at which it suspended. – Tanner Sansbury Jun 01 '15 at 14:13
  • 1
    @CppNoob When learning coroutines, I find that those who try to understand coroutines in terms of other constructs struggle when compared to those who approach it with a clean slate. It may be worth considering going through the [Boost.Coroutine](http://www.boost.org/doc/libs/1_58_0/libs/coroutine/doc/html/index.html) documentation with a clean slate. In particular, the Overview and Introduction may help explain coroutines, and the Motivation covers some great use cases, including a specific Boost.Asio example. – Tanner Sansbury Jun 01 '15 at 14:28
  • @TannerSansbury `"When the timer.async_wait() yields the coroutine, control jumps to the left stack immediately after the point at which started the coroutine. " ` during this step when control is on left,what is executing the body of async_wait???if service is ran in main,is there another thread in which body of async_wait is executed???if async_wait will be ran in main,would this prevent returning immediately to left after start,and the body of async_wait will be first executed in main then handler called then return to left"service run"??? – ahmed allam Apr 20 '20 at 16:54