关于协程
协程(coroutine)是一种程序组件,它允许多个入口点用于暂停和恢复执行的函数,可以在单个线程内实现多任务的近似并发执行。与线程相比,协程的切换完全由用户控制,不需要操作系统的介入,从而降低了开销。
协程的关键特性是它们可以"挂起"(yield),之后可以在某个时刻重新"恢复"(resume)。这种挂起和恢复的能力使得协程可以用来实现协作式多任务,即在协程控制下主动让出CPU,以便其他协程可以运行。
为什么使用协程
- 轻量级并发:协程比线程更轻量级,协程之间的切换不需要操作系统的介入。这意味着可以创建数以万计的协程而不会像线程那样消耗大量资源。
- 简化异步编程:协程可以使异步代码的编写更加直观和易于理解。通过使用同步编程风格编写异步代码,可以避免回调地狱(callback hell)和维护复杂的异步逻辑。
- 单线程:因为是单线程,所以协程间访问相同资源无需加锁,相比线程池更加安全。
基于上述优点,可以把巨量的需求拆解成巨量的协程(又称任务),让需求与实现一一对应,而无需考虑巨量任务造成性能下降。
c/c++现有方案
-
Boost.Coroutine :Boost库提供了一个
coroutine
库,它支持简单的协程功能。不过,这个库在Boost 1.70之后已经被废弃,转而推荐使用新的coroutine2
库。 -
Boost.Coroutine2:这是Boost库中的新协程库,提供了更加现代的协程支持,但它仍然不是基于C++20标准的协程。
-
C++20 Coroutine:随着C++20标准的发布,一些库开始基于标准的协程进行开发,提供更加完善的协程支持。
-
ucontext:Unix操作系统中提供的一组函数和宏,用于创建、保存和恢复用户级上下文,但qnx操作系统已经裁剪掉。
-
libucontext :第三方基于汇编和c实现的ucontext的多平台支持开源库。部分架构支持
FREESTANDING
模式,允许在裸机上使用新lib(newlib)部署。本轮子基于该库实现。
关于libucontext
libucontext用法和ucontext基本完全一致,不一样的地方是他提供了汇编和c源码,可以不依赖操作系统的ucontext,因此可以方便移植到不支持ucontext的操作系统,例如qnx。
libucontext
提供的 API 通常模仿了传统的 ucontext
API,包括以下函数:
-
libucontext_getcontext
:获取当前执行的上下文,并将其保存在一个libucontext_ucontext_t
结构中。 -
libucontext_setcontext
:设置当前执行的上下文,从libucontext_ucontext_t
结构中恢复之前保存的上下文,并继续执行。 -
libucontext_makecontext
:创建一个新的上下文,指定一个函数和其参数,以便在调用libucontext_setcontext
或libucontext_swapcontext
时执行。 -
libucontext_swapcontext
:保存当前上下文到第一个参数指向的libucontext_ucontext_t
结构中,并设置第二个参数指向的上下文为当前上下文,从而实现上下文的切换。
以上API提供了协程切换的基础。
SingleThreadScheduler
SingleThreadScheduler是一个基于libucontext实现的c++协程调度器,旨在为用户提供自定义周期的任务调度。SingleThreadScheduler调度的最小单位是任务Task,每个Task认为是一个协程。SingleThreadScheduler会把所有任务加到自己的任务列表里,并通过libucontext_makecontext
为每个任务创建入口context、设置返回context指针,初始化完成后,SingleThreadScheduler会找出最小过期时间的任务,并切换到该任务,任务应在合适的时候执行task_yield
函数放弃cpu返回SingleThreadScheduler,周期往复。
task.h关键代码-初始化
cpp
void task_init(libucontext_ucontext_t* context) {
stack = std::make_unique<uint8_t[]>(stack_size); //申请内存作为栈
libucontext_getcontext(&entry_context); //makecontext需要通过getcontext初始化一个ucontext
entry_context.uc_stack.ss_sp = stack.get(); //传递栈指针
entry_context.uc_stack.ss_size = stack_size; //传递栈大小
entry_context.uc_link = context; //当新context运行完后的返回context,这里返回调度器的context
libucontext_makecontext(&entry_context, (void (*)()) task_helper, 1, this); //制造一个指向task_helper的context,并传递本类指针
return_context = context; //设置返回context
next_run_time = get_timestamp_ms(); //设置next_run_time为当前时间戳,即期待马上切换该context
}
single_thread_scheduler.h关键代码-调度
cpp
void scheduler() {
//对所有Task进行初始化(申请栈,makecontext,保存调度器的context的指针)
for (auto& task : tasks) {
task->task_init(&context);
}
while (true) {
if (!running) {
//收到退出信号后,等待所有task执行结束
bool all_task_finished = true;
for (auto& task : tasks) {
all_task_finished &= task->get_finished();
}
if (all_task_finished) {
break;
}
}
if (tasks.empty()) {
std::this_thread::sleep_for(std::chrono::milliseconds(free_sleep_time));
continue;
}
//找出最小过期时间任务的索引
std::size_t index{0};
int64_t min_next_run_time = INT64_MAX;
for (std::size_t i=0; i<tasks.size(); ++i) {
if (tasks[i]->get_next_run_time() < min_next_run_time) {
min_next_run_time = tasks[i]->get_next_run_time();
index = i;
}
}
//计算next_run_time最小的任务(tasks[index])的过期时间(sleep_time)
int64_t sleep_time = tasks[index]->get_next_run_time() - Task::get_timestamp_ms();
//如果过期时间<=1ms并且该任务是周期任务,切换到task并运行,task应在适当的时候主动执行task_yield放弃cpu返回当前断点
if ((sleep_time <= 1) && (!tasks[index]->get_finished())) {
libucontext_swapcontext(&context, tasks[index]->get_entry_context());
continue;
}
//如果过期时间比较长,则重置过期时间,防止错过退出信号
if (sleep_time > free_sleep_time) {
sleep_time = free_sleep_time;
}
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
}
}
例子
cpp
#include <single_thread_scheduler.h>
class Example1 : public Task {
public:
Example1() {
period_ms = 150U;
}
void period_task() override {
auto current = get_timestamp_ms();
printf("Example1 before yield timestamp %ld\n", get_timestamp_ms());
task_yield(2000);
printf("Example1 after yield timestamp %ld\n", get_timestamp_ms());
last = current;
}
private:
int64_t last{0};
};
class Example2 : public Task {
public:
Example2() {
period_ms = 100U;
}
void period_task() override {
auto current = get_timestamp_ms();
printf("Example2 %p %ld\n", this, current - last);
last = current;
}
private:
int64_t last{0};
};
int main() {
auto scheduler = std::make_unique<SingleThreadScheduler>("hello");
scheduler->add_task(std::make_unique<Example1>());
scheduler->add_task(std::make_unique<Example2>());
scheduler->init();
std::this_thread::sleep_for(std::chrono::seconds(30));
}
例子log
textile
./coroutine
Example2 0x561c4daf91c0 32106875
Example1 before yield timestamp 32106926
Example2 0x561c4daf91c0 100
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 100
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 100
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 100
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 100
Example2 0x561c4daf91c0 101
Example2 0x561c4daf91c0 100
Example2 0x561c4daf91c0 101
Example1 after yield timestamp 32108926
从log里可以看出, Example1的task_yield(2000)不影响Example2的运行,Example1 yield之后能从断点恢复。
源码
https://gitee.com/cyy1205/coroutine
参考文献
[1].C++一分钟之-认识协程(coroutine)-腾讯云开发者社区-腾讯云
[2].ucontext的简单介绍 - ink19 - 博客园
[4].Boost.Context库简介及Boost.Coroutine协程使用方式 - 掘金
[5].boost.context-1.61版本的设计模型变化-腾讯云开发者社区-腾讯云
[6].GitHub - kaniini/libucontext: ucontext implementation featuring glibc-compatible ABI