当前位置: 首页 > news >正文

从 0 到 1:写一个能跑在大体量应用后台的 C++ 协程库

从 0 到 1:写一个能跑在大体量应用后台的 C++ 协程库

作者:monsoon
日期:2025-08-06

“进程太重,线程太闹,协程刚刚好。”


1. 为什么是协程?

在简历满天飞的校招季,面试官已经听腻了“我做过 WebServer”。
如何差异化?答案就是——造一个协程库

  • 轻量:用户态切换,ns 级上下文切换。
  • 易用:同步写法,异步性能。
  • 通用:可以插进任何项目里给 WebServer 加 Buff。

2. 协程长什么样?

我们先写出协程最核心的三样东西:

组件作用对应代码
Fiber协程对象class Fiber
Scheduler调度器class Scheduler
IOManagerIO + 定时器class IOManager : public Scheduler, public TimerManager

3. Fiber:一条协程的一生

3.1 数据结构

class Fiber : public std::enable_shared_from_this<Fiber> {
public:enum State { READY, RUNNING, TERM };
private:uint64_t        m_id        = 0;uint32_t        m_stacksize = 0;      // 128 k 固定栈void*           m_stack     = nullptr;ucontext_t      m_ctx;std::function<void()> m_cb;State           m_state     = READY;bool            m_runInScheduler = false; // 是否走调度器
};
  • 有栈协程,每个协程独占一块 malloc 出来的内存作为运行栈。
  • ucontext 四件套:getcontext / makecontext / swapcontext 完成上下文切换。

3.2 创建与入口

Fiber::Fiber(std::function<void()> cb, size_t stacksize, bool run_in_scheduler): m_cb(std::move(cb)), m_runInScheduler(run_in_scheduler) {m_stacksize = stacksize ? stacksize : g_fiber_stack_size;m_stack = StackAllocator::Alloc(m_stacksize);getcontext(&m_ctx);m_ctx.uc_stack.ss_sp    = m_stack;m_ctx.uc_stack.ss_size  = m_stacksize;m_ctx.uc_link           = nullptr;makecontext(&m_ctx, &Fiber::MainFunc, 0);
}

关键点:makecontext 把入口函数绑到栈上,协程启动时从 MainFunc 开始跑。

3.3 上下文切换

void Fiber::resume() {SetThis(this);m_state = RUNNING;if (m_runInScheduler) {swapcontext(&Scheduler::GetMainFiber()->m_ctx, &m_ctx);} else {swapcontext(&t_thread_fiber->m_ctx, &m_ctx);}
}void Fiber::yield() {if (m_runInScheduler) {swapcontext(&m_ctx, &Scheduler::GetMainFiber()->m_ctx);} else {swapcontext(&m_ctx, &t_thread_fiber->m_ctx);}
}
  • 非对称模型:任何协程只能与“调度协程”或“线程主协程”切换,不会丢失根上下文。

4. Scheduler:让线程变成线程池

4.1 线程局部变量

static thread_local Scheduler* t_scheduler       = nullptr;
static thread_local Fiber*     t_scheduler_fiber = nullptr;
  • 每个工作线程保存两份上下文:
    • 当前运行协程
    • 调度协程(idle 状态阻塞在 epoll_wait

4.2 任务队列

struct ScheduleTask {Fiber::ptr        fiber;std::function<void()> cb;int               thread = -1;   // 指定线程 or ANY
};
std::list<ScheduleTask> m_tasks;

4.3 调度主循环

void Scheduler::run() {Fiber::ptr idle(new Fiber(std::bind(&Scheduler::idle, this)));while (!stopping()) {ScheduleTask task = popTask();if (task.fiber) {task.fiber->resume();   // 协程跑完或 yield 回来} else if (task.cb) {task.cb();              // 普通函数} else {idle->resume();         // 没活干,进 idle}}
}

5. IOManager:IO + 定时器一把梭

5.1 epoll 封装

int  m_epfd;
int  m_tickleFds[2];   // pipe 用于唤醒
  • tickle:当有新任务时,写 1 字节到 pipe,让 epoll_wait 立刻返回。
  • idle:把 epoll_wait 的超时设置成“下一个定时器到期时间”,既等 IO 也等定时器

5.2 事件三元组

struct FdContext {struct EventContext {Scheduler*          scheduler;Fiber::ptr          fiber;std::function<void()> cb;};EventContext read;EventContext write;int fd;Event events = NONE;
};

6. Hook:把阻塞变非阻塞

6.1 原理

利用 LD_PRELOADdlsym(RTLD_NEXT, "read") 劫持系统调用,内部走 IOManager 的事件循环。

6.2 代码示例:sleep

unsigned int sleep(unsigned int seconds) {if (!sylar::t_hook_enable) return sleep_f(seconds);auto iom = IOManager::GetThis();iom->addTimer(seconds * 1000,[fiber = Fiber::GetThis()] { fiber->resume(); });Fiber::GetThis()->yield();return 0;
}
  • 协程注册一个定时器 → 立即 yield → 调度器去干别的活 → 定时器触发 → resume 回来继续执行。

6.3 connect_with_timeout

int connect_with_timeout(int fd, const sockaddr* addr, socklen_t len, uint64_t timeout_ms) {...iom->addEvent(fd, WRITE);               // 注册可写事件iom->addConditionTimer(timeout_ms, [fd]{ iom->cancelEvent(fd, WRITE); });Fiber::GetThis()->yield();              // 让出 CPU...
}

7. 实战:Echo Server 对比测试

方案QPS(单核)内存(MB)备注
原生 epoll190k20纯 C
libevent185k28事件库
本项目188k35协程栈成本
libco180k30共享栈

结论:在 IO 密集场景下,协程几乎不损失性能,却带来 同步代码 + 异步效率 的极致体验。


8. 可扩展方向

  1. 共享栈:参考 libco,减少内存碎片。
  2. 协程池:避免频繁 new / delete Fiber。
  3. 优先级调度:仿 Linux CFS,支持权重。
  4. 嵌套协程:维护 pCallStack[128],允许深层调用链。

9. 思考与踩坑

  • 边缘触发 + 非阻塞 pipe:否则 read 可能永久阻塞。
  • 线程安全 epoll:多线程共用 epfd 无需加锁。
  • 异常安全:协程入口抛异常怎么办?框架不吞异常,务必 try/catch
  • 调试:gdb 无法单步协程?用 Fiber::GetFiberId() 打印日志。

10. 一句话总结

造完轮子才发现:协程不是魔法,它只是把“阻塞等待”翻译成“注册事件 + yield + resume”。
但正是这层翻译,让 C++ 服务器在单线程上也能开出百万并发之花。


Reference

代码来自github.com/sylar-yin/sylar

http://www.lryc.cn/news/611679.html

相关文章:

  • 怎么免费建立自己的网站步骤
  • Docker 数据存储路径(解决默认docker路径位置磁盘空间不足的情况)
  • 家庭宽带中的服务器如何被外网访问?
  • RequestBodyAdviceAdapter是什么有什么用
  • [Linux]学习笔记系列 -- [arm][debug]
  • MCP 协议:AI 时代的 “万能转接头”,从 “手动粘贴” 到 “万能接口”:MCP 协议如何重构 AI 工具调用规则?
  • Linux 中 Git 操作大全
  • Go语言 单元测试
  • 鸿蒙app 开发中 全局弹窗类的封装 基于PromptAction
  • LazyLLM教程 | 第3讲:大模型怎么玩:用LazyLLM带你理解调用逻辑与Prompt魔法!
  • AI_提示词Prompt
  • MCP-PromptX AI小说创作使用教程
  • 百度智能云给“数字人”发工牌
  • 纯血鸿蒙(HarmonyOS NEXT)应用开发完全指南
  • HarmonyOS 5 入门系列-鸿蒙HarmonyOS示例项目讲解
  • day20|学习前端
  • 合同全生命周期管理系统是什么?
  • 基于php的个人健康管理系统设计与实现/vue/php开发
  • 数据结构(四)内核链表、栈与队列
  • JAVA无人系统台球茶室棋牌室系统支持H5小程序APP公众号源码
  • Python Pandas.lreshape函数解析与实战教程
  • 基于Simulink/MWORKS的文字与开关量混合传输系统设计
  • Godot ------ 初级人物血条制作02
  • 符合网络安全的汽车OTA软件更新分发机制
  • DHCP 服务器练习
  • Nacos配置中心和数据隔离在idea中的实现
  • R4周打卡——Pytorch实现 LSTM火灾预测
  • Ansys Discovery 2025R2的主要增强功能:CFD仿真的亮点
  • 批量打印Excel条形码
  • 西门子PLC基础指令6:读取时钟指令、设置时钟指令、使能含义与注意