tinyCoroLab Docs
直通tinyCoroLab源码直通tinyCoro源码
latest
latest
  • 🚀开启你的tinyCoroLab之旅!
  • 📮致读者
  • 👊C++协程入门
    • 协程初探
    • 有栈协程VS无栈协程
    • C++协程入门实践
    • 从编译器视角揭秘 C++ 协程
    • 协程调用优化
  • 🌟认识io_uring
  • 📖tinyCoroLab实验介绍
  • 📘Lab1 构建协程任务封装
    • Lab1 构建协程任务封装
    • Lab1 实验解析
  • 📗Lab2 构建任务执行引擎
    • Lab2a 构建任务执行引擎engine
    • Lab2a 实验解析
    • Lab2b 构建任务执行引擎context
    • Lab2b 实验解析
  • 📙Lab3 封装异步I/O执行模块
  • 📕Lab4 构建基础协程同步组件
    • Lab4pre 如何构建协程同步组件
    • Lab4a 构建基础协程同步组件event
    • Lab4a 实验解析
    • Lab4b 构建基础协程同步组件latch
    • Lab4b 实验解析
    • Lab4c 构建基础协程同步组件wait_group
    • Lab4c 实验解析
    • Lab4d 构建基础协程同步组件mutex
    • Lab4d 实验解析
  • 📓Lab5 构建进阶协程同步组件
    • Lab5a(选做) 构建进阶协程同步组件when_all
    • Lab5a 实验解析
    • Lab5b 构建进阶协程同步组件condition_variable
    • Lab5b 实验解析
    • Lab5c 构建进阶协程同步组件channel
    • Lab5c 实验解析
  • ✨tinyCoro Bonus Lab
  • 🎯tinyCoro悬赏令
  • ⚔️面试实战
    • 面试实战
    • tinyCoro面试相关问题
  • 🚩实验总结-终点亦是起点
  • 🪐番外杂谈
    • 从编译器视角揭秘 C++ 协程
    • 协程调用优化
  • 📌更新日志
Powered by GitBook
On this page
  • tinyCoroLab2a 实验简介
  • 📖lab2a 任务书
  • 实验前置讲解
  • ⚠️注意事项
  • 实验任务书
  • 🔖测试
  1. 📗Lab2 构建任务执行引擎

Lab2a 构建任务执行引擎engine

tinyCoroLab2a 实验简介

本节我们将正式开始 tinyCoroLab2a,即构建任务执行引擎的子模块 engine。tinyCoro 的执行引擎是由 context 和 engine 两部分组成的,engine 作为执行引擎的核心部分,负责存储并执行所有 task 以及异步 I/O,因此要想让 tinyCoro 真正对外提供服务的第一步便是完成 engine 的实现。

💡怎么去理解 context 和 engine? 这里做一个比喻,把执行引擎看做战场的士兵,task 是待执行的任务,那么 engine 便是士兵手里的武器,context 则是士兵本人,一个士兵配备一把武器,只有装备了武器的士兵才能去执行任务,而后续的 scheduler 可以看作是司令官,负责收集士兵传递的情报并向士兵派发任务。

预备知识

⚠️预备知识即在实验开始前你应该已经掌握的知识,且在知识铺垫章节中均有涉及

  • io_uring 的概念以及 liburing 的使用

  • eventfd 的概念及使用

  • C++ 协程句柄的使用

📖lab2a 任务书

实验前置讲解

本节实验涉及到的核心文件为include/coro/engine.hpp和src/engine.cpp,实验者需要预先打开文件浏览大致代码结构,下面针对该文件内容进行讲解。

首先是 engine 的成员变量 uring_proxy,我们打开其所在文件include/coro/uring_proxy.hpp,该文件本质上是对 liburing 的二次封装,读者应该了解过代理模式,通过这种方法可以降低 liburing 与 tinyCoro 之间的耦合性并简化 liburing 的使用。实验者可以阅读注释来理解 uring_proxy 各个方法的功能,并且可以在此基础上进行拓展。uring_proxy 使用的 io_uring 是与 eventfd 绑定的,虽然在知识铺垫章节已经讲过二者之间的搭配,但这里还是要重点强调一下:从 eventfd 读取的值并不能反映已完成的 I/O 数量,只能表示有已完成的 I/O,具体原因已在知识铺垫章节讲过。

然后是 engine 存储 task 的数据结构m_task_queue,具体存储单元为协程句柄。task 是由 engine 自身消费的,但 task 的生产可能来自别的线程,因此可以看作单消费者 - 多生产者模型,engine 默认选用第三方库atomic_queue提供的一个高性能的多生产者 - 多消费者无锁环形缓冲队列,队列的大小由 tinyCoro 的配置参数决定,当然实验者也可以替换为别的数据结构。

💡如何 engine 的任务队列已满,且此时自身工作线程向自身派发任务,会不会导致永久性阻塞? 会的,tinyCoro 在 1.1 版本修复了该问题,但实验暂不强制实验者解决该问题,测试也暂不涉及

其次是 engine 用来存储 io_uring cqe 的数组m_urc,在批量从 io_uring 取出 cqe 的时候需要用到。

关于 engine 的其它接口均有注释且在具体任务书中会详细讲解,下图是 engine 与外部交互的逻辑图,读者可在接下来的具体任务书中结合该图来理解。

engine 处理的任务包括两种:协程计算任务和 IO 任务。处理协程计算任务即从任务队列中取出协程句柄并恢复运行,上文中提到 engine 可能会收到别的线程投递的协程任务,因此对于任务队列的操作即存取任务必须是线程安全的。

但 IO 任务比较特殊,协程发起 IO 操作时会从 io_uring 获取 sqe,填充信息后通知 engine 有 IO 待提交,当 IO 完成后 engine 会负责恢复该协程,这里实验者可以自行选择是由当前 engine 恢复执行还是交付给调度器来选择。由于 io_uring 的官方推荐做法是一个线程持有一个 io_uring 实例且不要跨线程操作 io_uring,不然会引发意外的错误。因此对于发起 IO 这一过程 tinyCoroLab 采取了单线程做法,即每一个协程发起 IO 时均是从当前工作线程绑定的 engine 获取 sqe,不会从别的线程窃取,因此这一过程不需要考虑线程安全,同理,对 engine 内部 IO 状态的改变也不需要考虑线程安全,实验者可根据 lab2a 的具体任务书来加深对该部分的理解。

⚠️注意事项

  • 请确保已阅读过tinyCoroLab Introduce章节。

  • 为了确保正确实现目标函数,实验者可能需要做一些额外操作:新增类、修改现有类的实现、补充现有类的方法和成员变量等操作,请遵循free-design 实验原则。

  • 你需要仔细评估待实现的接口是否需要是线程安全的。

  • 任何导致测试卡住、崩溃等无法使测试顺利通过的情况都表明你的代码存在问题。

实验任务书

🧑‍💻Task #1 - 完善 engine 初始化以及异步 I/O 支持

任务目标

对于 engine 的初始化与析构是通过调用init()和deinit()成员函数实现的,实验者需要自行完善这部分逻辑来确保资源的正确初始化与释放,任何需要被 init 或者 deinit 的成员变量均应该被正确处理。

需要注意的是在实验介绍章节提到include/coro/meta_info.hpp文件存储了线程局部变量作为协程运行的上下文,定义如下:

struct CORO_ALIGN local_info
{
    context* ctx{nullptr};
    engine*  egn{nullptr};
};
inline thread_local local_info linfo;

这里用 thread_local 限定的linfo即线程局部变量,实验者应当在init()函数对egn进行赋值来使得协程在运行时可以通过local_engine()获取当前线程绑定的 engine。

从 engine 交互逻辑图中我们不难看出外部想要执行异步 I/O 需要调用get_free_urs()获取 sqe 后再调用add_io_submit()告诉 engine 刚刚新添加了一个 I/O 任务等待被提交,因此你应该在 engine 内部新增变量来存储待提交的 I/O 数,至于 engine 如何执行 I/O 会在接下来的 Task 中涉及。

涉及文件

  • include/coro/engine.hpp

  • src/engine.cpp

待实现函数

  • coro::detail::engine::init()

  • coro::detail::engine::deinit()

  • coro::detail::engine::get_free_urs()

  • coro::detail::engine::add_io_submit()

补充说明

提交 I/O 的线程可能非当前线程,因此你需要确保整个 lab2a 中关于 IO 运行状态的处理是线程安全的,至于 liburing 其本身实现就是线程安全的。

🧑‍💻Task #2 - 完善 engine 任务执行能力

任务目标

首先是对于外部调用 engine 的submit_task(coroutine_handle<> handle)来提交任务,实验者应该保证该任务会被存储到任务队列。

然后是外部的工作线程会调用 engine 的 API 来驱动 engine 执行任务,详情如下:

  • ready():工作线程用来判断 engine 的任务队列是否还有待执行的任务。

  • num_task_schedule():得到当前 engine 的任务队列还有多少任务。

  • schedule():engine 会从任务队列中弹出一个任务并作为返回值返回,该函数会被已经实现好的exec_one_task()函数调用。

  • empty_io():工作线程调用此函数来判断 engine 内部是否还有未处理的 I/O 任务,这包括未提交和正在执行但未完成的 I/O,因此实验者可能需要额外变量来记录 I/O 任务的执行状态。如果empty_io()返回了 false 那么即使工作线程收到停止信号也不会直接停止,因为这表明还有任务没有执行完成。

  • poll_submit():这是 engine 最为核心的函数,实验者需要在此函数中实现对 I/O 的提交以及处理已经完成的 I/O,对于从 io_uring 取出的 cqe,实验者仅需要对其调用已经实现的handle_cqe_entry()函数即可。而在处理 I/O 的过程中也应该变更 I/O 执行状态,这部分逻辑实验者自行设计。

对于核心的poll_submit()函数,这里务必强调一下,实验者应该清楚 uring_proxy 中的 io_uring 是与 eventfd 绑定的,为什么要绑定 eventfd 呢?因为如果不这样做,在工作线程没有任何计算任务和 I/O 任务的情况下会一直循环执行 engine 的 API,这会浪费 CPU 资源,因此在poll_submit()函数中应当尝试对 eventfd 进行阻塞读,如果 I/O 事件完成向 eventfd 写值后会唤醒工作线程,此时处理已完成的 I/O 事件。那么问题又来了,任务队列有任务但没有 I/O 任务,工作线程那不就一直卡在读 eventfd 上了吗?是的,因此你需要在恰当的时机对 eventfd 写值来达到“唤醒”工作线程的效果。

当然上述是我提示给你的思路,但不管怎样请记住,工作线程是通过循环调用 engine 的 API 来驱动 engine 执行任务的!tinyCoro 实现的循环方式是在一个循环中先处理所有计算任务再调用poll_submit()处理 I/O 任务。实验者必须确保外部工作线程在循环的过程中不会卡住且顺利处理完成所有任务,这样才能在收到停止信号后优雅退出。

💡对 eventfd 操作涉及到系统调用会不会很耗时?阻塞在 eventfd 读上岂不是会降低效率? eventfd 的读写非常轻量基本在 1us 左右,阻塞在 eventfd 读表明此时没有任何任务待处理(怎么?没任务还不让闲着😠?),正好利用阻塞使得工作线程让出执行权来给其他线程。

💡上述提到的外部工作线程在循环中先处理所有计算任务再调用poll_submit()处理 I/O 任务,该怎么理解呢? 这是不少读者反馈的问题,我特意准备下图来供读者理解。注意下图中红色字的部分,计算任务本质是从队列中取出协程句柄并恢复运行,IO 任务分为提交和处理,处理即将 cqe 绑定的句柄送至任务队列,这相当于恢复了因发起 IO 而 suspend 的协程。不过对于 IO 的具体处理当前只需要调用现有的handle_cqe_entry()即可,更多的细节会在 lab3 演示。

涉及文件

  • include/coro/engine.hpp

  • src/engine.cpp

待实现函数

  • coro::detail::engine::submit_task(coroutine_handle<> handle)

  • coro::detail::engine::ready()

  • coro::detail::engine::num_task_schedule()

  • coro::detail::engine::schedule()

  • coro::detail::engine::empty_io()

  • coro::detail::engine::poll_submit()

🔖测试

功能测试

功能测试场景主要针对:

  • 单线程执行计算任务

  • 单线程执行 IO 任务

  • 工作线程驱动下的计算任务执行

  • 工作线程驱动下的 IO 任务执行

  • 工作线程驱动下的混合任务执行

“工作线程驱动”表示测试会开启额外的线程并利用循环的方式驱动 engine 执行任务,对于测试中的 IO 任务采用了 liburing 提供的 nop-io,提交到 io_uring 后会立刻产生 cqe。

完成本节实验后,实验者请在构建目录下执行下列指令来构建以及运行测试程序:

make build-lab2a # 构建
make test-lab2a # 运行

内存安全测试

在构建目录下运行下列指令来执行内存安全测试:

make memtest-lab2a

测试通过会提示 pass,不通过会给出 valgrind 的输出文件位置,请实验者根据该文件排查内存故障。

Previous📗Lab2 构建任务执行引擎NextLab2a 实验解析

Last updated 1 month ago

engine_core
loop_cycle