✨tinyCoro Bonus Lab
Bonus Lab 实验简介
首先恭喜各位通过了 tinyCoroLab 设下的重重考验顺利打造出了属于自己的异步协程库👍!相信大家在完成实验后一定可以感受到 C++ 协程搭配 io_uring 所展现出的强大实力,还记得 tinyCoroLab 的主页上有下面一段话吗?
tinyCoroLab的更新会持续进行,并致力于打造对标工业标准的代码质量和性能
当然,现有的 tinyCoroLab 距离工业级可用还有很长一段距离要走,并通过不断的更新,最终达到:
完备的功能: 丰富的功能满足用户需求
强悍的性能: 在擅长的领域以超高性能碾压竞品
高质量代码: 简洁清晰且易拓展的代码设计,严格遵循设计模式
对此,作者特此增设 Bonus Lab 环节,为有兴趣完善自己的 tinyCoro 的读者指明一条道路,本节实验会从 功能 和 性能 两个维度为读者提出多个任务,不过与前面的常规实验不同,本节涉及的任务具有较强的思维发散性,测试程序较难也暂未构建。另外,任务会被添加标注来表明作者本人的 tinyCoro 是否已完成该任务。
Bonus LabA: 性能篇
LabA1: 优化协程内存分配
作者的tinyCoro是否完成该任务❎
作者本人曾针对协程写了一篇深入分析其内部原理的文章从编译器视角揭秘 C++ 协程,建议读者一定要认真阅读从而更深入的了解协程。该文章提到了 C++ 协程依赖于动态内存分配,虽然编译器足够智能使得协程在其完整的生命周期内仅需要一次动态内存分配,但仅此一次足以造成性能瓶颈,因此为了提升协程库的运行性能,首要便是优化协程的动态内存分配。
tinyCoroLab在1.2版本支持对协程内存分配器进行自定义,首先读者可打开配置文件config/config.h.in,其中关于内存分配器的配置如下:
// ====================== memory allocator configuration ====================
// 只有启用该宏,用户自定义的内存分配器才会生效,否则使用默认libc内存分配器
#define ENABLE_MEMORY_ALLOC
// 配置内存分配器
constexpr coro::detail::memory_allocator kMemoryAllocator = coro::detail::memory_allocator::std_allocator;
然后打开具体的内存分配器实现文件include/coro/allocator/memory.hpp,首先为开发者提供了一个内存分配器初始模板,代码如下:
// 初始模板
template<coro::detail::memory_allocator alloc_strategy>
class memory_allocator
{
public:
struct config
{
// 特化后的每个模板都应有自己的配置类
};
public:
// 内存分配器资源析构
~memory_allocator() = default;
// 根据配置类初始化
auto init(config config) -> void {}
// 分配内存
auto allocate(size_t size) -> void* { return nullptr; }
// 释放内存
auto release(void* ptr) -> void {}
};
// 用户直接使用mem_alloc_config作为配置类
using mem_alloc_config = memory_allocator<coro::config::kMemoryAllocator>::config;
初始模板保留了必要的接口,同时外部也仅会使用这些接口与内存分配器交互,开发者特化模板后可以添加自定义接口但必须保留初始模板的接口,这样开发者在拓展出新的内存分配器后仅需要更改配置文件中的内存分配器设置即可,符合设计模式中的开闭原则。
该文件也提供了一个样例直接利用标准库内存分配器特化出一个模板类,其实现较为简单,那么如何将其应用到协程内存分配中呢?开发者可打开文件include/coro/task.hpp,核心代码如下:
struct promise_base
{
// codes
#ifdef ENABLE_MEMORY_ALLOC
// C++标准为开发者提供了定制内存分配的方法,只需要重载
// promise的new和delete方法即可
void* operator new(std::size_t size)
{
// ginfo中的mem_alloc即内存分配器,
// scheduler会在初始化时将内存分配器实例注册到ginfo中
return ::coro::detail::ginfo.mem_alloc->allocate(size);
}
void operator delete(void* ptr, [[CORO_MAYBE_UNUSED]] std::size_t size)
{
::coro::detail::ginfo.mem_alloc->release(ptr);
}
#endif
// codes
};
开发者在开发新的内存分配器时,最好利用一下当前的上下文环境,比如执行引擎中的 context 和 engine 会在启动后将自身注册到全局变量 ginfo 中,此时内存分配器收到内存分配请求,可以根据 ginfo 判断当前请求属于哪个 context,这样在多个 context 存在的情况下可以做区分对待,避免多线程竞争,大名鼎鼎的 tcmalloc 就是通过线程缓存来提升内存分配效率的。
🔖测试
🚧测试包含功能、内存安全以及性能测试,目前正在设计中,读者完成任务后可先自行验证。
LabA2: 执行引擎任务窃取机制
作者的tinyCoro是否完成该任务❎
当前 tinyCoroLab 的执行引擎仅支持从本地任务队列获取任务,但这样容易造成多个 context 存在的情况下任务分配不均导致部分 context 空闲从而使得cpu利用率下降。
现有的协程调度模型比如golang的gmp,其核心设计之一便是任务窃取,允许当前工作线程从别的工作线程的任务队列窃取任务,开发者可参考gmp的设计改进 tinyCoroLab 的执行引擎,关于gmp更详细的信息读者可点击刘丹冰大佬发布的视频Golang深入理解GPM模型。
由于任务窃取机制属于开放性设计,因此不再过多赘述,读者自由发挥即可。
🔖测试
🚧测试包含功能、内存安全以及性能测试,目前正在设计中,读者完成任务后可先自行验证。
Bonus LabB: 功能篇
LabB1: io_uring sqe消费限制
作者的tinyCoro是否完成该任务❎
在使用 io_uring 时我们需要初始化一个实例并指定队列大小,而队列大小也影响着 io_uring 可以对外提供的 sqe 的数量,在 tinyCoroLab3 中我们定义了 base_awaiter 作为所有 IO awaiter 的基类,base_awaiter 在初始化时会自动向当前线程绑定的 context 所持有的 io_uring 实例获取一个 sqe ,但是当 io_uring 实例的sqe消费速率高于 cqe 返还速率时,sqe 最终会耗尽,base_awaiter 此时获取的sqe是个空指针,而 tinyCoroLab 现有的代码并未考虑这种情况。
基于上述问题,有必要对 io_uring 进行 sqe 消费限制,这个限制可以通过暴力来解决,比如检查到获取的 sqe 为空那么直接返还错误终止此次 IO 请求,或者是更优雅的做法,让当前发起 IO 的协程处于 suspend 状态,一旦有空闲的 sqe 便可以唤醒该协程, 当然,具体的设计取决于你~
🔖测试
🚧测试包含功能、内存安全以及性能测试,目前正在设计中,读者完成任务后可先自行验证。
LabB2: 修复执行引擎任务提交死锁
作者的tinyCoro是否完成该任务✅
在 tinyCoroLab 现有的设计中,如果协程在运行时抛出一个新的任务会交由 scheduler,然后 scheduler 根据 dispatcher 的任务分配策略选择一个 context 处理该任务并将任务投递至其绑定的任务队列,而这样的设计存在一个bug,其会导致程序永久性阻塞。
假设当前的协程所关联的 context A 其任务队列已满,该协程此时向 scheduler 派发任务,dispatcher 恰好选中了context A,此时向其任务队列提交任务,但任务队列已满,而投递任务使用了第三方库 atomic_queue 提供的 push 方法,该方法会永久性阻塞直到调用成功,但此时 context A 的任务队列是不可能减少的,即 push 方法会永远陷入阻塞。
基于上述问题,读者可尝试优化设计来避免这种情况的发生,由于是开放性设计,读者自由发挥即可~
💡作者的 tinyCoro 通过将 push 方法改为 try_push 方法,一旦 try_push 返回 false,那么直接原地执行该任务,但如果仅仅靠原地执行很容易导致栈溢出问题,因此作者在配置文件中加入了配置项来限制原地执行的最大深度,一旦超过最大深度那么协程任务会被直接丢弃并打印告警日志。
💡一种更推荐的做法是设置一个全局队列,若遇到上述情况,直接将协程任务丢至全局队列中即可。
🔖测试
🚧测试包含功能、内存安全以及性能测试,目前正在设计中,读者完成任务后可先自行验证。
LabB3: 高精度定时器设计
作者的tinyCoro是否完成该任务❎
对于用户一个常见的需求是开启一个定时任务来定时执行某些业务,定时任务的表现形式可以是cron表达式,即以此作为函数参数。那么怎么实现定时逻辑呢?
借助 liburing 提供的 io_uring_prep_timeout,我们可以将超时事件视为一种 IO,超时触发会使 io_uring 产出对应的 cqe,tinyCoroLab 也利用该函数封装了简单的超时函数,具体代码可见include/coro/timer.hpp,使用样例可见examples/timer.cpp。
实现了超时逻辑后定时任务也不难实现,但问题在于如何保证高精度定时?如果将定时事件与常规协程任务一同处理,那么很大概率定时事件会因为当前执行引擎正在处理其他任务而被延后处理,或许可以通过为定时事件单独开辟一个任务队列?当然,具体的设计取决于你~
🔖测试
🚧测试包含功能、内存安全以及性能测试,目前正在设计中,读者完成任务后可先自行验证。
总结
若您有任何关于提升 tinyCoroLab 的想法,欢迎通过在 github 向 tinyCoroLab 提交 issue 或者与作者本人私聊的形式贡献自己的 idea!
Last updated