对于操作系统而言,进程是最小的资源管理单元,线程是最小的执行单元。对于一个线程,在使用中,性能可能会受到以下因素的影响:
- 涉及到线程阻塞状态和可运行状态之间的切换
- 线程上下文的切换
- 同步锁
- ……
所以引入协程——更加轻量级的线程。
就像进程和线程的关系一样,一个进程可以拥有多个线程,一个线程可以拥有多个协程。
协程是在用户态执行的,所以不会像线程切换那样消耗资源,使性能得到提升。可以说,线程是一个特殊的函数,这个函数可以在某个地方挂起,并且可以在挂起处继续运行。一个线程内可以由多个这样的特殊的函数在运行,但是一个线程的多个协程的运行是串行的。如果是多核CPU,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内协程却绝对是串行的。毕竟协程仍然是一个函数,一个线程内可以运行多个函数,但是每个函数都是串行运行的。当一个协程运行时,其他的协程必须被挂起。
C语言上下文切换的库ucontext:
typedef struct ucontex {
stack_t uc_stack; // 当前上下文要用到的栈
mcontext_t uc_mctx; // 跟硬件相关的信息
struct ucontext *uc_link; //保存当前context结束后继续执行的context记录
sigset_t uc_sigmask; // 存的上下文的运行期间要屏蔽的信号集合
}ucontext_t;
// stack_t结构体
typedef struct {
void *ss_sp; // 栈的起始位置
int ss_size; // 栈大小
int ss_flags; // 标志
}stack_t;
- int getcontext(ucontext_t *ucp); 获取当前上下文
- int setcontext(const ucontext_t *ucp); 将当前程序上下文置为ucp指向的上下文
- void makecontext(ucontext_t *ucp, void (*func)(), int argc, …);修改ucp指向的上下文, 上下文转而指向函数func, argc是传入参数个数, …为传入参数(直接就是参数, 不用指针),也就是构造上下文。
- int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);保存当前上下文, 切换到新的上下文, 等于是先执行getcontext(oucp) 再执行setcontext(ucp)
写个程序感受一波 getcontext() 和 setcontext()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>
int main() {
int i;
ucontext_t ctx;
getcontext(&ctx);
printf("i = %d\n",i++);
sleep(1);
setcontext(&ctx);
}
先解释一波:首先 getcontext() 获取上下文,然后往下边执行,执行到 setcontext() 就会去执行getcontext() 获取到的上下文,也就是说就会回到 getcontext() 所在位置继续往下执行,从而形成一个循环,循环的打印 i 的值,结果如下:
再来个程序感受一波 makecontext() :
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>
void fun() {
printf("fun()\n");
}
int main() {
int i = 1;
char *stack = (char*)malloc(sizeof(char) * 8192);
ucontext_t ctx_main;
ucontext_t ctx_fun;
getcontext(&ctx_main);
getcontext(&ctx_fun);
printf("i = %d\n",i++);
sleep(1);
ctx_fun.uc_stack.ss_sp = stack;
ctx_fun.uc_stack.ss_size = 8192;
ctx_fun.uc_stack.ss_flags = 0;
ctx_fun.uc_link = &ctx_main;
makecontext(&ctx_fun, fun, 0);
setcontext(&ctx_fun);
printf("main exit\n");
return 0;
}
解释一波:main 函数首先 getcontext(),获取 main 函数的上下文 ctx_main,ctx_fun 在下边使用了 makecontext(),使得 ctx_fun 的上下文变成了 fun()函数,所以程序会先打印一个 i = X,接着去 fun 函数打印 fun() 因为 ctx_fun 的连接的 ctx_main 所以又陷入一个循环,打印完 i 的值,打印 fun()
来个代码感受 swapcontext();
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <ucontext.h>
ucontext_t ctx_f1;
ucontext_t ctx_f2;
ucontext_t ctx_main;
void fun1() {
printf("fun1() start\n");
swapcontext(&ctx_f1, &ctx_f2);
printf("fun1() end\n");
}
void fun2() {
printf("fun2() start\n");
swapcontext(&ctx_f2, &ctx_f1);
printf("fun2() end\n");
}
int main() {
char stack1[1024 * 8];
char stack2[1024 * 8];
getcontext(&ctx_main);
getcontext(&ctx_f1);
getcontext(&ctx_f2);
ctx_f1.uc_stack.ss_sp = stack1;
ctx_f1.uc_stack.ss_size = 8192;
ctx_f1.uc_stack.ss_flags = 0;
ctx_f1.uc_link = &ctx_f2;
makecontext(&ctx_f1, fun1, 0);
ctx_f2.uc_stack.ss_sp = stack2;
ctx_f2.uc_stack.ss_size = 8192;
ctx_f2.uc_stack.ss_flags = 0;
ctx_f2.uc_link = &ctx_main;
makecontext(&ctx_f2, fun2, 0);
swapcontext(&ctx_main, &ctx_f1);
printf("main exit\n");
}
解释:
首先初始化 ctx_f1 ,它的后继是 ctx_f2,接着 makecontext() 构造上下文,ctx_f1 的上下文是 fun1() 函数;
初始化 ctx_f2,它的后继是 ctx_main ,接着 makecontext() 构造上下文, ctx_f2 的上下文是 fun2() 函数。
执行流程:首先主函数遇到 swapcontext(&ctx_main, &ctx_f1),切换到 fun1() 函数,进入 fun1() 函数遇到 swapcontext(&ctx_f1, &ctx_f2) ,切换到 fun2() 函数,进入fun2() 函数遇到 swapcontext(&ctx_f2, &ctx_f1) ,返回 fun1()函数,fun1() 函数执行完,去执行 fun1() 函数的后继 fun2() ,fun2() 函数执行完,去执行 fun2() 函数的后继主函数,效果如下:
----------------------------------------------------分割线----------------------------------------------------
所以现在要实现协程,协程的结构体中应该有:回调函数、协程上下文、协程栈、协程状态。还得有一个协程调度器来控制所有的协程,具体代码如下:
enum State {
DEAD,
READY,
RUNNING,
SUSPEND
};
struct schedule;
// 协程结构体
typedef struct {
// 回调函数
void *(*call_back)(struct schedule *s, void* args);
// 回调函数参数
void *args;
// 协程上下文
ucontext_t ctx;
// 协程栈
char stack[STACKSZ];
// 协程状态
enum State state;
}coroutine_t;
// 协程调度器
typedef struct schedule {
// 所有协程
coroutine_t **coroutines;
// 当前正在运行的协程,如果没有正在运行的协程为 -1
int current_id;
// 最大下标
int max_id;
// 主流程上下文
ucontext_t ctx_main;
}schedule_t;
协程还得有以下功能的函数,具体的实现,参照最开始的链接
// 创建协程调度器
schedule_t *schedule_creat();
// 协程执行函数
static void* main_fun(schedule_t *s);
// 创建协程,返回当前协程在调度器的下标
int coroutine_creat(schedule_t *s, void *(*call_back)(schedule_t *,void *args),void *args);
// 让出 CPU
void coroutine_yield(schedule_t *s);
// 恢复 CPU
void coroutine_resume(schedule_t *s, int id);
// 删除协程
static void delete_coroutine(schedule_t *s, int id);
// 释放调度器
void schedule_destroy(schedule_t *s);
// 判断所有协程都运行完了
int schedule_finish(schedule_t *s);
// 启动协程
void coroutine_running(schedule_t *s, int id);
有了以上函数的功能,使用协程实现网络服务器的具体思路如下:
做到一请求一协程,而不是一请求一线程。因为协程调度器里边是用数组保存的所有的协程,所以可以用一个循环一直去遍历这个数组,当哪个协程需要执行的时候,就执行,如果让出CPU了,就循环往后遍历看下一个。在同步的情况下实现了异步。用一个 current_id 来判断当前协程是否运行(如果不运行 current_id 置为 -1)。