I'm reading through the documentation for boost::asio
and came across this example:
void foo(boost::asio::yield_context yield)
{
size_t n = socket.async_read_some(buffer, yield);
// ...
}
I am quite perplexed:
async_read_some
does not block.I see two options for how boost::asio
implements this:
socket.async_read_some
is called, this function itself calls a function in the event loop which attempts to find any events which are ready to be processed and tries to call these callbacks.setjmp
/longjmp
magic registers a callback for this function and it is returned to at a later point.Both options seem to not work:
...
boost_internal_magic
async_read_some
foo
...
main
Because we haven't yet returned from foo. But if all functions are called using yield_context
I don't see how we'd actually register a callback because there is no code point to jump to as a function call.
asyncio
is to run multiple concurrent "agents" or "coroutines" or what have you on the same thread. Each agent would need its own stack, so this doesn't seem to work either.How does a boost::asio::yield_context
turn a function into a callback which can be registered? What would minimal no dependency c++ code look like which has the same functionality?
The trick is that yield_context
employs stackful coroutines. The coroutine has it's own separate stack, which is preserved across suspend/resume cycles.
The technical implementation of this is in Boost Context. Earlier versions of Asio actually implemented this on top of another library: Boost Coroutine (which uses Boost Context under the hood).
Asio 1.24.0 / Boost 1.80 changed to skip the middle man:
When targeting C++11 and later, these functions are implemented in terms of Boost.Context directly. The existing overloads have been retained but are deprecated.
You can read up on the technical workings of Boost Context at that library's documentation: https://www.boost.org/doc/libs/1_86_0/libs/context/doc/html/index.html
To zoom on on the particulars of the implementation, you can follow where the implementation leads, e.g. in:
#define BOOST_ASIO_DISABLE_BOOST_COROUTINE 1
#include <boost/asio.hpp>
#include <boost/asio/spawn.hpp>
namespace asio = boost::asio;
using namespace std::chrono_literals;
int main() {
asio::io_context ctx;
spawn(
ctx,
[](asio::yield_context yield) {
asio::steady_timer tim(yield.get_executor(), 3s);
tim.async_wait(yield);
},
asio::detached);
ctx.run();
}
Which would lead you to boost/asio/impl/spawn.hpp
, and shows you that the non-Boost.Coroutine implementation actually uses Boost.Context's fiber (BOOST_ASIO_HAS_BOOST_CONTEXT_FIBER
).
// Spawned thread implementation using Boost.Context's fiber.
class spawned_fiber_thread : public spawned_thread_base
{
public:
typedef boost::context::fiber fiber_type;
spawned_fiber_thread(fiber_type&& caller)
: caller_(static_cast<fiber_type&&>(caller)),
on_suspend_fn_(0),
on_suspend_arg_(0)
{
}
// ...
From here you can follow the implementation details arbitrarily deep, right into the various assembly code routines to help in the CPU-specific context switching: https://github.com/boostorg/context/tree/develop/src/asm