📖tinyCoroLab实验介绍
Last updated
Last updated
tinyCoroLab是一门以tinyCoro为基础的实验课程,而tinyCoro是一个 Linux 系统环境下的以C++20 协程技术和 Linux io_uring 技术相结合的高性能异步协程库。高效且全能的 io_uring 和 C++20 无栈协程的轻量级切换相组合使得tinyCoro可以轻松应对 I/O 密集型负载,而 C++20 协程的特性使得用户可以以同步的方式编写异步执行的代码,大大降低了后期维护的工作量,且代码逻辑非常简单且清晰,除此外tinyCoro 还提供了协程安全组件,以协程 suspend 代替线程阻塞便于用户构建协程安全且高效的代码。 下图展示了tinyCoro的任务调度机制:
tinyCoro的设计并不复杂,每个执行引擎拥有一个工作线程和 io_uring 实例来处理外部任务,而调度器通过创建多个执行引擎来提高系统的并发能力,设计框架图如下所示:
经过测试由tinyCoro实现的 echo server 在 1kbyte 负载和 100 个并发连接下可达到 100wQPS,关于tinyCoro更详细的信息请访问 GitHub 主页。
tinyCoroLab的设计灵感来自于广为人知的 CMU15445 数据库内核实验和 MIT6.824 分布式实验,它们通过让学生编写指定接口的实现并通过大量的测试验证正确性来使得学生提高代码能力的同时学习到特定的领域知识,高难度的实验带来的是巨大的代码级和知识级的能力提升,这也使得这两门实验课程广受学生好评。而tinyCoroLab沿袭这种设计思路,通过将tinyCoro的实现拆分成多个子部分来构成 5 节实验课程,包括:
lab1: 构建协程任务封装,包括 lab1 一个子实验
lab2: 构建任务执行引擎,包括 lab2a 和 lab2b 两个子实验
lab3: 封装异步 I/O 执行模块,包括 lab3 一个子实验
lab4: 构建基础协程同步组件,包括 lab4a,lab4b,lab4c,lab4d 四个子实验
lab5: 构建进阶协程同步组件,包括 lab5a,lab5b,lab5c 三个子实验
在上述实验的基础上tinyCoroLab提供了 200+ 项功能测试和内存安全测试来考察用户实现的功能逻辑正确性和内存安全性,并且 lab4 和 lab5 额外新增了性能测试通过 googlebenchmark 将基线模型与用户实现做对比来衡量用户实现的性能。除此之外tinyCoroLab在 cmake 中定义了大量的指令来简化实验流程并提供一键生成 perf 性能分析火焰图的脚本,使得实验者可以专心于实现实验内容。
tinyCoroLab并不像 CMU15445 和 mit6.824 课程那样涉及特定领域复杂的知识,因此难度稍弱,但其本身涉及到多线程下的高并发因此仍旧对实验者的代码能力仍有较强考验。tinyCoroLab核心功能代码共计约 2000 行,测试加样例共计约 4000 行,而作为tinyCoroLab的开源实现版本tinyCoro在实验的基础上添加了约 700 行功能代码,因此实验者代码量预计在 700 行以下,耗时预计一个星期左右。
通过完成该实验,你将收获:
简历新增一个新颖、有技术含量且被深度量化过的项目。
更加熟练的 C++ 编程技巧,以及对 C++ 新标准的应用。
对强大的 io_uring 异步 I/O 技术的掌握。
对多线程编程有更深入的理解。
本实验学习了 GitHub 开源且高 star 的 C++ 项目的项目组织方式,如果你愿意研究该部分内容,你还将收获:
一个标准的通过 cmake 构建 C++ 项目的方式。
在项目中熟练运用单元测试、性能测试以及性能分析工具。
python version >= 3.7: 部分测试脚本需要依赖 python 执行,请确保本地命令行运行 python 或者 python3 不会报错
valgrind version >= 3.18.1: 部分读者反映内存测试出错是因为 valgrind 版本过旧,3.18.1 为作者测试的一个可以正常运行的版本,如果读者本地内存测试出错可以尝试更新到该版本
由于tinyCoroLab使用了只在 Linux 下支持的 liburing,因此用户需要准备 Linux 系统环境并使用尽量新的内核,wsl 以及虚拟机均可,并预先安装好 cmake 以及支持 C++20 标准的 gcc 编译器。
首先实验者需要克隆tinyCoroLab到本地:
git clone https://github.com/sakurs2/tinyCoroLab.git
初始化所有子模块:
cd tinyCoroLab
git submodule update --init --recursive
按下列步骤安装 liburing:
⚠️最好不要使用系统包管理器来安装 liburing,因为包管理器通常是旧版本,可能会出现功能 bug,同时也请确保 tinyCoroLab 的 cmake 搜索到的 liburing 是最新版
cd third_party/liburing/
./configure --cc=gcc --cxx=g++;
make -j$(nproc);
make liburing.pc
sudo make install;
上述步骤结束后回到tinyCoroLab主目录,按下列步骤构建tinyCoroLab:
mkdir build
cd build
cmake ..
make -j$(nproc)
编译成功后即表示tinyCoroLab的环境正式搭建完毕。
$ tree -d -A -I third_party -I build -I .vscode -I resource -I temp
.
├── benchmark # 压测文件存放目录
│ ├── base_model # 基线模型(暂时废弃)
│ └── tinycoro_model # tinyCoro 压测模型
├── benchtests # lab4 和 lab5 的 googlebenchmark 测试文件存放目录
├── config # 配置文件存放目录
├── examples # 使用 tinyCoro 构建的样例程序存放目录
├── include # tinyCoroLab 核心头文件目录
│ └── coro
│ ├── comp # 协程同步组件存放目录
│ ├── concepts # C++ concepts 文件存放目录
│ ├── detail # 辅助文件如类型定义和基础数据结构存放目录
│ └── net # 网络编程实现文件存放目录
├── scripts # 实验脚本文件存放目录
├── src # tinyCoroLab 核心源文件目录
│ ├── comp
│ └── net
└── tests # 功能测试代码文件存放目录
上面的目录树展示了tinyCoroLab的核心目录结构并用注释表明了各个目录的用途,下面我们针对各个模块的核心文件并讲解用途。
该文件存放了tinyCoroLab的配置项,由于项目使用了 cmake 传递的变量所以配置文件是.h.in 的文件格式并由 cmake 指令生成项目真正使用的配置文件 config.h,因此实验者对配置的任何修改必须在config.h.in文件里,而对于 config.h 文件的修改会在重新执行 cmake 构建指令后被覆盖掉。
关于各个配置项的内容文件内均添加了注释,实验者保持默认设置参数即可。
1.1 版本之前存在运行模式这一概念,在短期运行模式下没有任何待执行的任务的 context 会立刻退出,那么 scheduler 便无法派发任务至该 context,这是不合理的。
在 1.1 版本kLongRunMode=true
参数已废弃,含糊不清的运行模式概念已被删除,现在 scheduler 会在全部 context 完成执行后统一发送退出信号,这样协程在运行过程中就可以自由的向 scheduler 派发任务了,具体示意图如下:
作为项目头文件存放目录,涉及到tinyCoroLab的核心功能,下面给出各个子模块的详细作用:
comp: 用于存放协程同步组件的文件,即 lab4 和 lab5 的实验内容。
concepts: C++20 提供了一种新的编译检查机制即 concepts。通过使用 Concepts,开发者可以明确指定模板参数必须满足的条件,从而提高代码的可读性和可维护性,同时编译器可以在编译时检查这些条件是否满足,避免在实例化模板时出现意外的错误?。而该目录存放了tinyCoroLab涉及到的 concepts 的定义。
detail/container.hpp: 该文件定义了一种数据存储容器,在实际开发中可能面临一种场景需要将数据暂存并延迟返回,而 C++ 协程在返回值的时候也是需要先将返回值存储到 promise 对象再利用 awaiter 返回值,读者可能会疑惑额外定义一个变量存储不就行了,但 C++ 是存在左值和右值以及移动和拷贝等概念的,如果值可被移动但你定义的容器仍然选择拷贝构造那么很容易造成性能损失,当然实际的情况可能更加复杂,所以定义该容器的目的就是为了对高效暂存数据提供一个统一的解决方案。原本该容器是libcoro中 task 的一部分,但在tinyCoroLab中有多个组件需要需要临时存储变量,所以将该逻辑从 task 中剥离单独作为一个模块,实验者在实验过程中也可根据需要使用该容器。
net: 这是tinyCoroLab的网络模块,目前只实现了简易的 tcp 支持,在实验者完成 lab1 和 lab2 之后就可以利用该模块实现针对 tcp 的网络 I/O 编程了。
atomic_que.hpp: 这里对第三方库AtomicQueue提供的高性能无锁环形队列进行了封装。
log.hpp: tinyCoroLab借助第三方库spdlog提供的线程安全日志功能,引入该头文件后使用log::info("hello {}","tinyCoro")
这种形式即可打印日志到终端,通过更改配置文件可实现额外打印到文件以及更改日志级别的功能。
meta_info.hpp: 存储全局共享以及线程局部变量的定义,有利于协程运行时获取上下文信息。
spinlock.hpp: 利用原子变量实现的一种自旋锁。
uring_proxy.hpp: 该文件对liburing进行了简单的二次封装,实验者可以在此基础上封装更多的功能。
utils.hpp: 存放工具函数。
task.hpp: 协程支持模块,所有任务的基本单元即一个 task,实验者将在 lab1 中完善该模块。
engine.hpp: engine 作为tinyCoroLab的心脏,即核心执行引擎,负责执行所有接收到的任务,包括异步执行 I/O 任务,实验者将在 lab2a 中完善该模块。
context.hpp: context 作为 engine 的封装,通过开启一个工作线程与 engine 交互确保所有接收到的任务都能顺利执行,实验者将在 lab2b 中完善该模块。
dispatcher.hpp: 该模块负责具体的任务分发逻辑,tinyCoroLab提供了最简单的 round-robin 式的任务分发方式。
scheduler.hpp: 采用单例模式实现的调度器,负责创建、运行以及终止批量 context 的运行,并根据 dispatcher 向各个 context 派发任务。
这四个文件夹存储了tinyCoroLab用于执行功能测试、内存安全测试、性能测试和实验辅助脚本的文件,具体使用方法后续会详细介绍。
tinyCoroLab默认配置了一条基于ubuntu系统的CI流水线,主要执行编译构建以及各项测试的流程,具体文件可见.github/workflows/ci-ubuntu.yml。
该CI流水线强制执行编译构建过程,对于各项测试根据用户配置文件选择性执行,用户可编辑scripts/CITests.yml来控制某项测试是否被执行。
在了解tinyCoroLab各个模块后,本节将会告诉大家如何正确的使用本实验精心构建并提供给实验者的各个技巧。注意本节提到的所有指令必须在构建目录下运行。
💡什么是构建目录? 在 cmake 组织的 C++ 项目根目录下新建 build 文件夹并在 build 下执行 cmake ..构建项目,这个 build 文件夹即构建目录。
当实验者在进行某项实验时,相关文件内会被添加特殊标记,标记不会对代码本身产生任何影响,其本身作为对实验的一种指示:
[[CORO_TEST_USED(labXX)]]
: 该标记一般出现在函数声明前,意为测试程序会使用此函数完成测试,请不要修改函数声明
TODO[labXX]
: 该标记一般出现在任意位置,表明此处可能需要实验者补充代码
在主目录的cmake 文件里有下面一行代码:
option(ENABLE_DEBUG_MODE "Enable debug mode" OFF)
上述代码的含义即tinyCoroLab默认采用 release 模式构建,而在 release 模式下项目会并开启-O3 优化,如果实验者需要切换到 debug 模式可以将上述ENABLE_DEBUG_MODE
改成默认值为 ON 或者利用下面的方式进行 cmake 构建:
mkdir build_debug
cd build_debug
cmake .. -DCMAKE_BUILD_TYPE=Debug
切换到 debug 模式后所有目标程序的构建将不再开启优化并添加-g 编译选型。建议读者针对 debug 模式和 release 模式分别在不同的文件夹下构建,不然因为 cmake 的 cache 机制在同一个文件夹下切换构建方式可能不会生效。
⚠️debug 的-O0 模式下 gcc 不会对协程调用进行尾部递归优化,这使得协程在被循环调用多次(例如 10w)后会出现栈溢出问题,由于是 gcc 的问题所以 tinyCoro 无法修复该 bug,实验者需要特殊注意
tinyCoroLab不仅在tests文件夹下为实验者提供了大量的测试,还提供了多个指令方便实验者进行测试,指令清单如下,
make build-tests
编译并列出所有测试
make check-tests
运行所有测试,依赖测试程序的构建
make build-lab1
编译 lab1 测试程序并列出测试清单
make test-lab1
运行 lab1 测试程序,依赖测试程序的构建
make build-lab2a
编译 lab2a 测试程序并列出测试清单
make test-lab2a
运行 lab2a 测试程序,依赖测试程序的构建
make build-la2b
编译 lab2b 测试程序并列出测试清单
make test-lab2b
运行 lab2b 测试程序,依赖测试程序的构建
make build-lab3
编译 lab3 测试程序并列出测试清单
make test-lab3
运行 lab3 测试程序,依赖测试程序的构建
make build-lab4a
编译 lab4a 测试程序并列出测试清单
make test-lab4a
运行 lab4a 测试程序,依赖测试程序的构建
make build-lab4b
编译 lab4b 测试程序并列出测试清单
make test-lab4b
运行 lab4b 测试程序,依赖测试程序的构建
make build-lab4c
编译 lab4c 测试程序并列出测试清单
make test-lab4c
运行 lab4c 测试程序,依赖测试程序的构建
make build-lab4d
编译 lab4d 测试程序并列出测试清单
make test-lab4d
运行 lab4d 测试程序,依赖测试程序的构建
make build-lab5a
编译 lab5a 测试程序并列出测试清单
make test-lab5a
运行 lab5a 测试程序,依赖测试程序的构建
make build-lab5b
编译 lab5b 测试程序并列出测试清单
make test-lab5b
运行 lab5b 测试程序,依赖测试程序的构建
make build-lab5c
编译 lab5c 测试程序并列出测试清单
make test-lab5c
运行 lab5c 测试程序,依赖测试程序的构建
💡指令中的依赖是啥意思? 比如make test-lab1是要执行测试程序,在没有依赖的情况下,如果测试程序未构建或者说发生更改后未重新构建,那么直接运行该指令会出现预期外的结果,因此需要添加依赖关系使得该命令执行时一定先构建测试程序,这种依赖关系是通过 cmake 文件添加的。
对于内存安全测试其逻辑是利用 valrgind 对功能测试的执行过程进行检查观察是否存在内存泄漏,并没有新增额外的测试逻辑,指令清单如下:
make memtest-lab1
运行 lab1 内存安全测试,依赖测试程序的构建
make memtest-lab2a
运行 lab2a 内存安全测试,依赖测试程序的构建
make memtest-lab2b
运行 lab2b 内存安全测试,依赖测试程序的构建
make memtest-lab4a
运行 lab4a 内存安全测试,依赖测试程序的构建
make memtest-lab4b
运行 lab4b 内存安全测试,依赖测试程序的构建
make memtest-lab4c
运行 lab4c 内存安全测试,依赖测试程序的构建
make memtest-lab4d
运行 lab4d 内存安全测试,依赖测试程序的构建
make memtest-lab5a
运行 lab5a 内存安全测试,依赖测试程序的构建
make memtest-lab5b
运行 lab5b 内存安全测试,依赖测试程序的构建
make memtest-lab5c
运行 lab5c 内存安全测试,依赖测试程序的构建
tinyCoroLab针对 lab4 和 lab5 增设了由 googlebenchmark 编写的性能测试并存放在benchtests文件夹下,每项实验的性能测试针对特定场景下的三种模型的性能对比,所有测试名称的前缀表示该测试使用的模型,具体前缀及含义如下所示:
thread_pool_stl_XX: 使用简单的线程池和 stl 组件。
coro_stl_XX: 使用 coro 调度器和 stl 组件。
coro_XX: 使用 coro 调度器和 coro 组件。
thread_pool_stl_XX和coro_stl_XX的性能差距可表示 coro 调度器的开销,coro_stl_XX和coro_XX的性能差距可表示 stl 组件和实验者实现的组件之间的性能开销差距。
性能测试同样提供了多个指令方便实验者进行测试,指令清单如下,
make build-benchtests
编译并列出所有测试指令
make benchbuild-lab4a
编译 lab4a 性能测试程序
make benchtest-lab4a
运行 lab4a 性能测试程序,依赖测试程序的构建
make benchbuild-lab4b
编译 lab4b 性能测试程序
make benchtest-lab4b
运行 lab4b 性能测试程序,依赖测试程序的构建
make benchbuild-lab4c
编译 lab4c 性能测试程序
make benchtest-lab4c
运行 lab4c 性能测试程序,依赖测试程序的构建
make benchbuild-lab4d
编译 lab4d 性能测试程序
make benchtest-lab4d
运行 lab4d 性能测试程序,依赖测试程序的构建
make benchbuild-lab5b
编译 lab5b 性能测试程序
make benchtest-lab5b
运行 lab5b 性能测试程序,依赖测试程序的构建
make benchbuild-lab5c
编译 lab5c 性能测试程序
make benchtest-lab5c
运行 lab5c 性能测试程序,依赖测试程序的构建
与上一小节提到的性能测试不同,I/O benchmark 测量的是使用 tinyCoro 构建的 echo server 在压测工具下所能达到的 QPS,实验者在完成 lab3 后可以使用该测试测算个人实现版本的 QPS,详细的测试过程请阅读benchmark/README.MD。
本实验为了方便实验者定位程序的性能瓶颈,额外增设了性能分析火焰图一键生成指令,关于该指令的使用请阅读scripts/README.MD。
实验者可能需要对某个逻辑编写功能自行测试但又不想动编写 cmake 代码来添加额外的编译过程,那么可以使用本实验提供的快速构建调试程序来解决这个问题。
在examples文件夹下新增coro_debug.cpp
,该文件默认被添加到.gitignore中,然后用 cmake 重新构建项目,此时项目会额外新增两个指令,清单如下:
make build-debug
编译 coro_debug.cpp 构建可执行文件 coro_debug
make run-debug
运行 coro_debug,依赖 coro_debug 的构建
使用上述两个指令可以避免实验者编写多余的 cmake 代码以及避免默认 make 构建全部目标带来的时间消耗。
tinyCoroLab 的测试涉及遵循三大原则:
明确:不论是单线程还是多线程,测试点的结果一定是明确且固定的。
理智:测试程序就像一个理智的用户一样,懂得如何正确使用 tinyCoro 编写代码,这也意味着所有的测试错误一定是归结于实验者编写的逻辑。
集中:测试程序会且仅会使用使用任务书提到的接口,其余均不会涉及。
有关测试的任何问题请及时向作者反馈。
实验者在实验过程中不必纠结某些预定义好的代码能不能改或者实验指导书没提到的地方能不能动,只需要记住一个原则:让所有测试可被成功编译。在此基础上你可以任意修改 tinyCoroLab 的内容(当然测试是不可以被更改的),比如新增文件、新增现有类方法、添加成员变量等等。