boost::asio::yield_context工作原理及极简无依赖实现问询
Boost.Asio yield_context 工作原理与极简实现解析
问题背景
阅读Boost.Asio文档时遇到如下示例代码:
void foo(boost::asio::yield_context yield) { size_t n = socket.async_read_some(buffer, yield); // ... }
核心困惑
- 按asyncio的设计逻辑,
async_read_some调用不应阻塞线程; - 这意味着需要将函数剩余逻辑注册为回调,待数据就绪后触发执行。
两种猜想及否定理由
猜想1:调用async_read_some时直接运行事件循环
此时调用栈结构为:
... boost_internal_magic async_read_some foo ... main
由于foo尚未返回,若所有函数都通过yield_context调用,找不到合适的代码点注册回调,逻辑不成立。
猜想2:基于setjmp/longjmp实现上下文跳转
线程仅拥有一个主栈,而asyncio的核心是单线程运行多个并发协程,每个协程需要独立的栈空间,这种方式无法满足多协程栈隔离的需求,逻辑不成立。
核心问题
- boost::asio::yield_context 是如何将函数转换为可注册的回调逻辑的?
- 具备相同功能的极简无依赖C++代码是怎样的?
工作原理解析
Boost.Asio的yield_context基于栈式协程(Stackful Coroutine)实现,底层依赖boost::coroutine库(C++20后可适配标准协程),核心是通过上下文切换保存当前函数的栈状态,而非传统的显式回调注册:
- 当把
yield_context传入async_read_some时,yield_context会生成一个隐式回调; async_read_some会将该回调注册到IO事件循环;- 随后
yield_context触发上下文切换,暂停当前foo函数的执行,将栈状态保存到独立的协程对象中,然后返回调用者(事件循环或上层协程); - 当IO事件就绪(数据可读),事件循环调用之前注册的回调,回调内部恢复
foo函数的栈状态,让foo从async_read_some的调用点继续执行,并返回读取到的字节数n。
这里的关键是:协程的栈状态被独立存储(不占用线程主栈),因此单线程可以同时存在多个暂停的协程,实现逻辑上的并发。
极简无依赖C++实现示例
以下是基于POSIX标准ucontext库(Windows可替换为Fiber API)的极简版本,模拟yield_context的核心逻辑:
#include <ucontext.h> #include <cstdio> #include <vector> #include <functional> // 协程上下文结构体,保存栈与执行状态 struct Coroutine { ucontext_t ctx; char stack[1024 * 64]; // 为协程分配独立栈空间 std::function<void()> func; bool finished = false; }; // 极简事件循环,存储待处理的IO回调 std::vector<std::function<void()>> pending_events; void run_events() { while (!pending_events.empty()) { auto event = pending_events.back(); pending_events.pop_back(); event(); } } // 模拟yield_context,负责协程的暂停与恢复 struct YieldContext { Coroutine* coro; void resume() { if (!coro->finished) { swapcontext(nullptr, &coro->ctx); } } }; // 模拟async_read_some逻辑:注册回调后暂停协程 size_t async_read_some(YieldContext yield) { // 注册IO就绪后的回调:恢复协程 pending_events.push_back([yield]() { yield.resume(); }); // 暂停当前协程,切换回调用者上下文 swapcontext(&yield.coro->ctx, nullptr); // 协程恢复后返回模拟的读取字节数 return 5; } // 被协程调度的业务函数 void foo(YieldContext yield) { size_t n = async_read_some(yield); printf("Read %zu bytes\n", n); } int main() { Coroutine coro; getcontext(&coro.ctx); // 配置协程栈 coro.ctx.uc_stack.ss_sp = coro.stack; coro.ctx.uc_stack.ss_size = sizeof(coro.stack); coro.ctx.uc_link = nullptr; // 将foo包装为协程入口函数 makecontext(&coro.ctx, [&]() { foo({&coro}); coro.finished = true; }, 0); // 启动协程,首次执行foo swapcontext(nullptr, &coro.ctx); // 运行事件循环,处理IO就绪事件 run_events(); return 0; }
代码说明
- 使用
ucontext_t实现上下文切换,为每个协程分配独立栈空间,实现栈隔离; YieldContext作为协程与IO操作的交互媒介,负责触发暂停与恢复;async_read_some模拟IO操作的回调注册逻辑,触发协程暂停;- 事件循环
run_events处理就绪事件,恢复对应的协程执行。
内容的提问来源于stack exchange,提问作者Jordan Bonecutter
相关产品推荐
相关产品推荐

