In short, you need to create a copy of handler, such as by posting it into the io_service
, before attempting to get the async_result
in order to keep the coroutine alive.
Boost.Asio prevents a non-resumable coroutine from indefinitely suspending by destroying the coroutine, resulting in the coroutine's stack to unwind. The coroutine object will throw boost::coroutines::detail::forced_unwind
during its destruction, causing the suspended stack to unwind. Asio accomplishes this by:
- The
yield_context
CompletionToken maintains a weak_ptr
to the coroutine.
- When the specialized
handler_type::type
handler is constructed, it obtains a shared_ptr
for the coroutine via the CompletionToken's weak_ptr
. When the handler is passed as the completion handler to asynchronous operations, the handler and its shared_ptr
are copied. When the handler is invoked, it resumes the coroutine.
- When invoking
async_result::get()
, the specialization will reset the coroutine shared_ptr
owned by the handler that was passed to async_result
during construction, and then yield the coroutine.
Here is an attempt to illustrate the execution of the code. 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_timer);
`-- dispatch a coroutine creator
into the io_service.
io_service.run();
|-- invoke the coroutine entry
| handler.
| |-- create coroutine
| | (count: 1)
| |-- start coroutine ----> my_timer()
: : |-- create handler1 (count: 2)
: : |-- create asnyc_result1(handler1)
: : |-- timer.async_wait(handler)
: : | |-- create handler2 (count: 3)
: : | |-- create async_result2(handler2)
: : | |-- create operation and copy
: : | | handler3 (count: 4)
: : | `-- async_result2.get()
: : | |-- handler2.reset() (count: 3)
| `-- return <---- | `-- yield
| `-- ~entry handler :
| (count: 2) :
|-- io_service has work (the :
| async_wait operation) :
| ...async wait completes... :
|-- invoke handler3 :
| |-- resume ----> |-- async_result1.get()
: : | |-- handler1.reset() (count: 1)
| `-- return <---- | `-- yield
| `-- ~handler3 : :
| | (count: 0) : :
| `-- ~coroutine() ----> | `-- throw forced_unwind
To fix this problem, handler
needs to be copied and invoked through asio_handler_invoke()
when it is time to resume the coroutine. For example, the following will post a completion handler1 into io_service
that invokes a copy of handler
:
timer.async_wait (handler);
timer.get_io_service().post(
std::bind([](decltype(handler) handler)
{
boost::system::error_code error;
// Handler must be invoked through asio_handler_invoke hooks
// to properly synchronize with the coroutine's execution
// context.
using boost::asio::asio_handler_invoke;
asio_handler_invoke(std::bind(handler, error), &handler);
}, handler)
);
return result.get ();
As demonstrated here, with this additional code, the output becomes:
my_timer enter
my_timer returns
1. The completion handler code can likely be cleaned up a bit, but as I was answering how to resume a Boost.Asio stackful coroutine from a different thread, I observed some compilers selecting the wrong asio_handler_invoke
hook.