协程,从编程的角度看来,可以理解为用户自己能控制和调度的线程,可以理解为用户级别的线程。一个线程可以有多个协程,一个进程也可以有多个协程。
协程实现协程的实现有多种方式
- 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 *); int InitFibersEnv(int maxFiberCount); int FreeFibersEnv(); int CreateFiber(FiberTask *task, void *arg); int ScheduleFiber(); #endif //TEST_FIBER_H
fiber.c 协程接口实现文件
#include "Fiber.h" #define _XOPEN_SOURCE #include#include #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执行void *PrintStrLine(void *str) { printf("%sn", (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++的集合,有些操作会出现奔溃问题,暂时理不清为什么。



