协程定义
协程,从编程的角度看来,可以理解为用户自己能控制和调度的线程,可以理解为用户级别的线程。一个线程可以有多个协程,一个进程也可以有多个协程。
协程实现
协程的实现有多种方式
- setjmp和longjmp
- sigsetjmp和siglongjmp
- ucontext函数族
本次采用ucontext实现,思路是:在进行函数跳转前,将获取当前调用者的上下文,并在当前调用者的上下文的基础上进行修改,制作新的函数堆栈,然后切换到函数的上下文。当函数执行结束,切换到调用者的上下文,让调用者继续执行协程调度。
void makecontext(ucontext_t *ucp, void (*func)(), int argc, …);
makecontext() 函数修改 ucp 指向的用户线程上下文,该上下文必须先前已通过调用 getcontext() 进行初始化,并为其分配了堆栈。 上下文被修改,以便通过使用提供的参数(类型为 int)调用 func() 来继续执行。 argc 参数必须等于提供给 makecontext() 的附加参数的数量,也等于提供给 func() 的参数数量,否则会出现不可知行为。
ucp->uc_link 参数必须在调用 makecontext() 之前初始化,如果ucp->uc_link等于 NULL,则函数返回后进程退出; 否则,函数要返回时隐式调用 setcontext(ucp->uc_link)。
int getcontext(ucontext_t *ucp);
getcontext() 函数将当前线程的执行上下文保存在 ucp 指向的结构中。 这个保存的上下文可以稍后通过调用 setcontext() 来恢复。
int setcontext(const ucontext_t *ucp);
setcontext() 函数使先前保存的线程上下文成为当前线程上下文,即当前上下文丢失并且 setcontext() 不返回。 相反,在 ucp 指定的上下文中继续执行,该上下文必须先前已通过调用 getcontext()、makecontext() 进行初始化。
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
swapcontext() 函数将当前线程上下文保存在 *oucp 中,并使 *ucp 成为当前活动的上下文。
实现如下
fiber.h 协程接口头文件
#ifndef TEST_FIBER_H
#define TEST_FIBER_H
//定义一个任务
typedef void *FiberTask(void *);
/**
* 初始化协程环境
* @param maxFiberCount 最大协程数量
* @return
*/
int InitFibersEnv(int maxFiberCount);
int FreeFibersEnv();
/**
* 创建一个协程
* @param task 传入一个任务
* @param arg 任务的参数
* @return
*/
int CreateFiber(FiberTask *task, void *arg);
/**
* 协程调度
* @return
*/
int ScheduleFiber();
#endif //TEST_FIBER_H
fiber.c 协程接口实现文件
#include "Fiber.h"
#define _XOPEN_SOURCE
#include <ucontext.h>
#include <stdlib.h>
#define StackSize (1024 * 10)
enum FiberStatusEnum {
FIBER_IDLE,
FIBER_RUNNING
};
/**
* 协程环境
*/
struct FiberEnv {
int fiberId;
ucontext_t ucontext;
FiberTask *fiberTask;
void *taskArg;//任务参数
char stackSpace[StackSize];
short status;//状态
};
struct FibersEnv {
int fiberCount;//协程数量
struct FiberEnv **fiberEnvs;//协程环境
ucontext_t ucontext;//主进程调度到上下文
};
struct FibersEnv *g_FiberEnv;
int InitFibersEnv(int maxFiberCount) {
g_FiberEnv = calloc(1, sizeof(struct FibersEnv));
g_FiberEnv->fiberCount = maxFiberCount;
g_FiberEnv->fiberEnvs = (struct FiberEnv **) calloc(maxFiberCount, sizeof(struct FiberEnv *));
for (int i = 0; i < maxFiberCount; ++i) {
g_FiberEnv->fiberEnvs[i] = calloc(1, sizeof(struct FiberEnv));
g_FiberEnv->fiberEnvs[i]->fiberId = i + 100;
}
return 0;
}
int FreeFibersEnv() {
if (!g_FiberEnv) {
return 0;
}
struct FiberEnv **p = g_FiberEnv->fiberEnvs;
while (*p != NULL) {
free(*p);
p++;
}
free(g_FiberEnv);
g_FiberEnv = NULL;
return 0;
}
struct FiberEnv *FindFreeFiberSlot() {
for (int i = 0; i < g_FiberEnv->fiberCount; ++i) {
if (g_FiberEnv->fiberEnvs[i] && g_FiberEnv->fiberEnvs[i]->status == FIBER_IDLE) {
return g_FiberEnv->fiberEnvs[i];
}
}
return NULL;
}
void RunTask(uint32_t highPtr, uint32_t lowPtr) {
uint64_t ptr = (uint64_t) highPtr << 32 | (uint64_t) lowPtr;
struct FiberEnv *fiberEnv = (struct FiberEnv *) ptr;
//执行任务
fiberEnv->fiberTask(fiberEnv->taskArg);
// printf("fiber[%d] exec ok!\n", fiberEnv->fiberId);
fiberEnv->status = FIBER_IDLE;
//任务执行完成之后,切换到调用者到上下文去执行
swapcontext(&fiberEnv->ucontext, &g_FiberEnv->ucontext);
}
//创建一个协程
int CreateFiber(FiberTask *task, void *arg) {
struct FiberEnv *fiberEnv = FindFreeFiberSlot();
fiberEnv->status = FIBER_RUNNING;
getcontext(&fiberEnv->ucontext);
fiberEnv->ucontext.uc_stack.ss_size = StackSize;
fiberEnv->ucontext.uc_stack.ss_sp = fiberEnv->stackSpace;
fiberEnv->fiberTask = task;
fiberEnv->taskArg = arg;
uint32_t lowPtr = (uint64_t) fiberEnv;
uint32_t highPtr = (uint64_t) fiberEnv >> 32;
makecontext(&fiberEnv->ucontext, (void (*)()) RunTask, 2, highPtr, lowPtr);
return 0;
}
int ScheduleFiber() {
for (int i = 0; i < g_FiberEnv->fiberCount; ++i) {
if (!g_FiberEnv->fiberEnvs[i]) {
continue;
}
swapcontext(&g_FiberEnv->ucontext, &g_FiberEnv->fiberEnvs[i]->ucontext);
}
return 0;
}
main.c 协程接口测试文件
#include "Fiber.h"
#include <stdio.h>
void *PrintStrLine(void *str) {
printf("%s\n", (char *) str);
return NULL;
}
int main(int argc, char **argv) {
InitFibersEnv(10);
CreateFiber(PrintStrLine, "hello-world1");
CreateFiber(PrintStrLine, "hello-world2");
CreateFiber(PrintStrLine, "hello-world3");
ScheduleFiber();
FreeFibersEnv();
printf("exec ok...\n");
return 0;
}
执行
gcc Fiber.c main.c -o testFiber
打印如下
hello-world1
hello-world2
hello-world3
exec ok...
注意
- 可以注意到,makecontext为协程入口函数绑定上下文的时候,当需要为函数传递指针等变量的时候,因为makecontext是可变参,函数内部统一用4个字节去处理(有些系统可能已经做到了兼容)。而指针变量是8个字节(64位系统),故需要将指针变量拆分成高位和低位去处理。
- 本文的实验环境位mac,mac默认的ucontext接口已经废弃,需要定义_XOPEN_SOURCE宏强制开启
- 在用mac实验的时候,如果是封装在C++的类函数中,并使用了C++的集合,有些操作会出现奔溃问题,暂时理不清为什么。