【高性能网络(3)】采用协程重构网络io


前言

  1. 为什么要有协程?
  2. 协程实现过程,原语操作(哪些?)
  3. 协程如何定义
  4. 调度器如何定义
  5. 调度器的执行策略
  6. posix api做到一致
  7. 协程的执行流程
  8. 协程的多核模式
  9. 协程的性能如何测试

一、协程设计原理

1.1 为什么要有协程?

一言以蔽之:同步的编程方式,异步的性能。

image.png

当采用同步方式,响应内容是逐步显示的;而采用异步方式,响应内容看不出先后次序。
其次响应时间异步(0.166s) << 同步(0.98s)

异步的缺点就是设计逻辑复杂,如果callback里面套了几层代码可读性差,不利于维护。

1.2 协程设计思路

  1. 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);
}
  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来实现函数跳转,需要注意的是第一次切换的时候上下文是对应的函数开始,之后就是直接跳转的对应的语句。

image.png

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 三种原语操作的优缺点

  1. 第一种实现较复杂,但跨平台性好,是标准C库
  2. 第二种实现简单,跨平台性一般,推荐这个
  3. 汇编,不限制操作系统,但限制芯片类型,即汇编的指令不一定相同

二、重构(hook)网络io

  1. 该网络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();
	}
}
  1. 采用协程解决网络io的基本思路
    1. 调度器中设计三种结构来管理调用时机
    2. create_coroutine中来注册跳转函数的上下文
    3. 在对应的网络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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值