I am unable to form a mental picture of how the control flow happens with spawn.
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
?
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?
In short:
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.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.