|
先看测试代码:https://godbolt.org/z/zYT6bon36
无栈协程的基本原理如果要实现无栈协程,有个核心问题,如何让一个函数能够在执行过程中暂停,并在之后从暂停点继续执行?
传统函数一旦开始执行,就会一直运行到返回为止,是不能中途暂停后再恢复的。
状态机方法最直接的解决方案是使用状态机。通过保存当前执行状态,函数可以在下次调用时从上次离开的地方继续:
int function(int *state) {
switch (*state) {
case0:
// 执行第一部分代码
*state = 1;
return0; // 返回,下次从case 1开始
case1:
// 执行第二部分代码
*state = 2;
return0; // 返回,下次从case 2开始
case2:
// 执行最后部分代码
*state = 0; // 重置状态
return1; // 完成
}
}
使用这个函数的代码可能是这样的:
int main() {
int state = 0; // 初始状态
while (function(&state) == 0) {
// 函数尚未完成,可以做其他事情
process_events();
sleep_or_yield();
}
// 函数执行完成
return 0;
}
这种方法的工作原理很清晰:使用一个状态变量跟踪执行进度每次调用时,从对应的状态继续执行执行一部分代码后,更新状态并返回函数返回值指示是否完成[/ol]虽然这种方法可行,但随着逻辑复杂度增加,状态数量也会迅速增长,状态管理肯定会变得非常复杂,代码变得难以维护。
利用switch-case实现更自然的控制流我们能否让暂停和恢复的代码看起来更自然,更接近普通的顺序代码?
这就是无栈协程的核心思想,我们其实可以通过巧妙使用C语言的switch-case语句,实现更优雅的解决方案。
看下面代码(这段代码就是protothreads宏展开后的代码):
static char task_function(state_t *state) {
staticint counter;
{
char yield_flag = 1;
switch (state->line) {
case0:; // 初始入口点
for (counter = 0; counter 32; ++counter) {
do {
do {
state->line = 81; // 保存当前行号
case81:; // 恢复点
if (!(resource_available())) {
return0; // 资源不可用,返回等待
}
} while (0);
consume_resource(); // 消费资源
} while (0);
process_data(); // 执行核心业务逻辑
release_resource(); // 释放资源
}
};
yield_flag = 0;
state->line = 0;
return3; // 任务完成
};
}
这段代码看起来很奇怪,但它实现了一个无栈协程!
核心代码这里利用了C语言switch-case语句的一个不常用特性:case标签可以出现在复合语句内部。这样我们可以在代码的任何位置设置恢复点。
核心机制解析代码的关键部分如下:
state->line = 81; // 保存当前行号
case 81:; // 恢复点
if (!(resource_available())) {
return 0; // 资源不可用,返回等待
}
这段代码实现了阻塞等待的功能:state->line = 81 保存当前执行位置的行号紧随其后的 case 81: 标签成为恢复点检查条件是否满足,不满足则返回当函数再次被调用时,switch (state->line) 会直接跳转到 case 81:,跳过之前已执行的代码[/ol]执行流程假设我们调用这个函数的流程如下:第一次调用:state->line 为0,执行从 case 0: 开始进入for循环,设置 state->line = 81检查资源是否可用,假设不可用返回0,表示需要等待[/ol]第二次调用:switch (state->line) 直接跳转到 case 81:再次检查资源是否可用,假设仍不可用再次返回0[/ol]第三次调用:再次跳转到 case 81:检查资源,假设现在可用了执行 consume_resource()然后执行 process_data() 和 release_resource()继续循环,设置下一个恢复点...[/ol]最终调用:当for循环完成后,执行 state->line = 0返回3,表示任务完成[/ol][/ol]为什么使用do-while(0)?宏中使用 do { ... } while (0) 很常见了。这种模式有几个作用:方便创建局部作用域,避免变量命名冲突确保宏展开后的代码结构正确可以在复合语句中使用多个语句[/ol]实现细节首先,需要一个结构体来保存执行状态:
typedef unsigned short line_t; // 行号类型
struct task_state {
line_t line; // 当前执行行号
};
接下来,我们定义任务的几种状态:
#define TASK_WAITING 0 // 任务等待中
#define TASK_YIELDED 1 // 任务主动让出
#define TASK_EXITED 2 // 任务退出
#define TASK_ENDED 3 // 任务正常结束
为了简化无栈协程的编写,可以定义一系列宏:
// 初始化任务状态
#define TASK_INIT(s) (s)->line = 0
// 开始任务函数体
#define TASK_BEGIN(s) { char yield_flag = 1; switch((s)->line) { case 0:
// 结束任务函数体
#define TASK_END(s) } yield_flag = 0; (s)->line = 0; return TASK_ENDED; }
// 等待条件满足
#define TASK_WAIT_UNTIL(s, condition) \
do { \
(s)->line = __LINE__;
case __LINE__:; \
if(!(condition)) { \
return TASK_WAITING; \
} \
} while(0)
// 主动让出执行权
#define TASK_YIELD(s) \
do { \
yield_flag = 0; \
(s)->line = __LINE__;
case __LINE__:; \
if(yield_flag == 0) { \
return TASK_YIELDED; \
} \
} while(0)
// 退出任务
#define TASK_EXIT(s) (s)->line = 0; return TASK_EXITED
// 调度任务
#define TASK_SCHEDULE(task_func) ((task_func)
使用上述宏,我们可以这样实现一个任务:
// 资源管理功能
staticint resource_count = 8;
staticint data_items = 0;
static int resource_available() {
return resource_count > 0;
}
static void consume_resource() {
resource_count--;
}
static void process_data() {
printf("Processing data item %d
", data_items++);
}
static void release_resource() {
resource_count++;
}
// 任务实现
static char task_function(struct task_state *state) {
staticint counter;
TASK_BEGIN(state);
for (counter = 0; counter 32; ++counter) {
TASK_WAIT_UNTIL(state, resource_available());
consume_resource();
process_data(); // 核心业务逻辑
// 模拟耗时操作,主动让出CPU
TASK_YIELD(state);
release_resource();
}
TASK_END(state);
}
// 主循环
int main() {
struct task_state state;
TASK_INIT(&state);
while (TASK_SCHEDULE(task_function(&state))) {
// 可以执行其他任务或休眠一段时间
usleep(10);
}
return0;
}
下面深入介绍几个关键地方。
行号作为恢复点这里使用了LINE宏来获取当前行号作为恢复点标识。这可以确保每个恢复点都有唯一的标识符,不需要手动分配。
当宏被预处理器展开时,LINE会被替换为实际的行号,例如:
TASK_WAIT_UNTIL(state, condition);
可能会被展开为:
do {
(state)->line = 42; // 假设这是第42行
case 42:;
if(!(condition)) {
return TASK_WAITING;
}
} while(0);
性能分析这种无栈协程实现的主要优势是极低的内存开销:每个协程仅需2字节:只需存储当前执行的行号无需独立栈空间:传统线程可能需要KB~MB级别的栈空间无堆内存使用:不需要动态内存分配[/ol]Protothreads的实现在掌握了基本原理后,可以构建一个更完善的无栈协程库,这里主要介绍Protothreads的设计思路和实现(上面主要也是Protothreads的设计)
Protothreads正是这样一个设计精巧的无栈协程库:
typedef unsigned short lc_t;
struct pt {
lc_t lc;
};
#define PT_WAITING 0
#define PT_YIELDED 1
#define PT_EXITED 2
#define PT_ENDED 3
/* 本地延续控制 */
#define LC_INIT(lc) (lc) = 0
#define LC_SET(lc) do { (lc) = __LINE__; case __LINE__:; } while(0)
#define LC_RESUME(lc) switch(lc) { case 0:
#define LC_END(lc) }
/* 协程控制 */
#define PT_INIT(pt) LC_INIT((pt)->lc)
#define PT_THREAD(name_args) static char name_args
#define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; LC_RESUME((pt)->lc)
#define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \
PT_INIT(pt); return PT_ENDED; }
#define PT_WAIT_UNTIL(pt, condition) \
do { \
LC_SET((pt)->lc); \
if(!(condition)) return PT_WAITING; \
} while(0)
#define PT_YIELD(pt) \
do { \
PT_YIELD_FLAG = 0; \
LC_SET((pt)->lc); \
if(PT_YIELD_FLAG == 0) return PT_YIELDED; \
} while(0)
#define PT_EXIT(pt) do { PT_INIT(pt); return PT_EXITED; } while(0)
#define PT_SCHEDULE(f) ((f)
/* 信号量实现 */
struct pt_sem {
unsignedint count;
};
#define PT_SEM_INIT(s, c) (s)->count = c
#define PT_SEM_WAIT(pt, s) \
do { \
PT_WAIT_UNTIL(pt, (s)->count > 0); \
--(s)->count; \
} while(0)
#define PT_SEM_SIGNAL(pt, s) ++(s)->count
这是一个典型的生产者-消费者的应用:
/* 示例协程 */
PT_THREAD(producer(struct pt *pt, struct pt_sem *sem)) {
staticint item = 0; // 必须使用静态变量[3,6](@ref)
PT_BEGIN(pt);
for(;;) {
PT_SEM_WAIT(pt, sem);
printf("Produced item %d
", item++);
PT_SEM_SIGNAL(pt, sem);
PT_YIELD(pt);
}
PT_END(pt);
}
PT_THREAD(consumer(struct pt *pt, struct pt_sem *sem)) {
staticint last = -1;
PT_BEGIN(pt);
for(;;) {
PT_SEM_WAIT(pt, sem);
printf("Consumed item %d
", last++);
PT_SEM_SIGNAL(pt, sem);
PT_YIELD(pt);
}
PT_END(pt);
}
int main() {
struct pt pt_prod, pt_cons;
struct pt_sem sem = {1}; // 二进制信号量初始化[2,6](@ref)
PT_INIT(&pt_prod);
PT_INIT(&pt_cons);
while(PT_SCHEDULE(producer(&pt_prod, &sem)) |
PT_SCHEDULE(consumer(&pt_cons, &sem))) {
printf("--- Scheduler Tick ---
");
}
return0;
}
点击此链接查看运行结果:https://godbolt.org/z/zYT6bon36
这里我们就是通过巧妙使用C语言的switch-case语句和宏,在不增加额外内存开销的情况下,实现类似线程的顺序控制流。
Protothreads源代码下载:https://dunkels.com/adam/pt/publications.html
现在不少朋友都在准备校招,常规的技术学习只是提高了代码能力,还没有提升从 0 到 1 整体做项目和解决问题的能力!
为此我们特别推出了C++训练营,带着你从 0 到 1 做 C++ 项目(你可以从项目池中任选项目),帮助你提升做项目的能力,提升从0到1的能力,熟悉做项目的完整流程,比如开发环境、编译脚本、架构设计、框架搭建、代码发布、问题调试、单元测试。
除了上面的,还有需求分析、项目规划、架构设计、任务拆解、时间规划、版本管理。另外做项目的过程中必然会遇到种种问题,可以逐步提升你的调试能力,分析问题的能力,掌握更多的调试手段。
遇到棘手的问题,我们还有专门的导师1V1答疑解惑,给出具体建议。
项目池里面的项目,是导师团队花费大量时间完成的,不仅有完整的代码及清晰的注释还有详细的文档和视频,并且有专门的项目导师答疑,完全不用担心自己学不会。
相信我,这些项目绝对能够让你进步巨大!下面是其中某三个项目的说明文档 |
|