文章目录
前言
- 为什么要有协程?
- 协程实现过程,原语操作(哪些?)
- 协程如何定义
- 调度器如何定义
- 调度器的执行策略
- posix api做到一致
- 协程的执行流程
- 协程的多核模式
- 协程的性能如何测试
一、协程设计原理
1.1 为什么要有协程?
一言以蔽之:同步的编程方式,异步的性能。

当采用同步方式,响应内容是逐步显示的;而采用异步方式,响应内容看不出先后次序。
其次响应时间异步(0.166s) << 同步(0.98s)。
而异步的缺点就是设计逻辑复杂,如果callback里面套了几层代码可读性差,不利于维护。
1.2 协程设计思路
- send函数不阻塞,通过switch不断切换来实现并发
// callback 是fd有数据接收的时候回调
callback() {
recv();
}
func() {
send_http(fd, callback);
switch(1, 2); // 切换到2执行
}
// 2
callback() {
recv();
}
func() {
send(fd, callback);
switch(2, 3);
}
// 3
callback() {
recv();
}
func() {
send(fd, callback);
switch(3, 1);
}
- async_xxx()是重构后的网络io,当检测到相应事件时会切换不同的执行函数
// 1
func() {
async_send(fd, callback);
async_recv(fd, buffer);
}
// 2
func() {
async_send(fd, callback);
async_recv(fd, buffer);
}
// 3
func() {
async_send(fd, callback);
async_recv(fd, buffer);
}
async_xxx() {
if (1 == poll(fd, 0)) {
switch();
}
}
1.3 实现协程的三种原语操作(switch)
1.3.1 setjmp/longjmp
#include <setjmp.h>
#include <stdio.h>
jmp_buf env;
void func(int arg) {
printf("func: %d\n", arg);
longjmp(env, ++arg);
}
int main() {
int ret = setjmp(env);
if (ret == 0) {
func(ret);
} else if (ret == 1) {
func(ret);
} else if (ret == 2) {
func(ret);
} else if (ret == 3) {
func(ret);
}
return 0;
}
以上通过控制arg的值来运行不同的func。
1.3.2 ucontext
#include <stdio.h>
#include <stdlib.h>
#include <ucontext.h>
ucontext_t ctx[2];
ucontext_t main_ctx;
int count = 0;
// coroutine1
void func1(void) {
while (count ++ < 20) {
printf("1\n");
swapcontext(&ctx[0], &ctx[1]);
// swapcontext(&ctx[0], &main_ctx);
printf("4\n");
}
}
// coroutine2
void func2(void) {
while (count ++ < 20) {
printf("2\n");
swapcontext(&ctx[1], &ctx[0]);
// swapcontext(&ctx[1], &main_ctx);
printf("5\n");
}
}
// scheduler
int main(void) {
char stack1[2048] = {0};
char stack2[2048] = {0};
getcontext(&ctx[0]);
ctx[0].uc_stack.ss_sp = stack1;
ctx[0].uc_stack.ss_size = sizeof(stack1);
ctx[0].uc_link = &main_ctx;
makecontext(&ctx[0], func1, 0);
getcontext(&ctx[1]);
ctx[1].uc_stack.ss_sp = stack2;
ctx[1].uc_stack.ss_size = sizeof(stack2);
ctx[1].uc_link = &main_ctx;
makecontext(&ctx[1], func2, 0);
printf("swapcontext\n");
swapcontext(&main_ctx, &ctx[0]);
printf("\n");
return 0;
}
通过ucontext来实现函数跳转,需要注意的是第一次切换的时候上下文是对应的函数开始,之后就是直接跳转的对应的语句。

1.3.3 asm(汇编)
store:
mov eax(co_a.a);
mov ebx(co_a.b);
load:
mov (co_b.a) eax;
mov (co_b.b) ebx;
1.3.4 三种原语操作的优缺点
- 第一种实现较复杂,但跨平台性好,是标准C库
- 第二种实现简单,跨平台性一般,推荐这个
- 汇编,不限制操作系统,但限制芯片类型,即汇编的指令不一定相同
二、重构(hook)网络io
- 该网络io不阻塞,当前fd还不可读时,向epoll注册事件,跳转到其他的相应处理函数。
// 初步认知
while (1) {
recv();
parser();
send();
}
hook -->
recv(fd, buffer, length) {
struct pollfd fds[1] = {0};
fds[0].fd = fd;
if (0 < poll(fd, 1, 0)) { // 可读
recv_f(fd, buffer, length);
} else {
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
swapcontext();
}
}
- 采用协程解决网络io的基本思路
- 调度器中设计三种结构来管理调用时机
- create_coroutine中来注册跳转函数的上下文
- 在对应的网络io中利用resume、yeild等重写结构
struct coroutine {
int fd;
ucontext_t ctx; // stack, stack_size, func
void *arg;
queue_nod(coroutine, ...) ready_queue;
rbtree_node(coroutine, ...) wait_rb;
rbtree_node(coroutine, ...) sleep_rb;
};
void func(void) {
}
typedef void *(*coroutine_entry)(void*)
int create_coroutine(co_id id, coroutine_entry, void *arg) {
struct coroutine *co = malloc(sizeof(struct coroutine));
co.....
makecontext(co->ctx, func, 0);
}
// scheduler
struct scheduler {
int epfd;
struct epoll_event events[];
queue_nod(coroutine, ...) ready_queue;
rbtree_node(coroutine, ...) wait_rb;
rbtree_node(coroutine, ...) sleep_rb;
};
总结
本文介绍了协程的基本设计思路以及实现协程的三种原语操作。最后介绍了重构网络io的基本思路,但是本文的内容仍然是较浅显的,还需要进一步的学习总结。
参考链接:
https://github.com/0voice
137

被折叠的 条评论
为什么被折叠?



