目录
C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂
1.为什么会有协程?
同步:执行一个操作之后,等待结果,然后才继续执行后续的操作。
异步:执行一个操作后,可以去执行其他的操作,然后等待通知再回来执行刚才没执行完的操作。
异步的优点:异步比同步性能更好
异步的缺点:异步不好理解,流程不清晰
协程,有异步的性能,同步的代码逻辑,,采用一种轻量级的协程来实现。协程解决了io操作程序复杂程度和性能之间的矛盾。写代码的方式是同步的,底层运行的逻辑是异步的。
2.协程的原语操作
原语是指实现某个特殊功能的且不能被打断的由若干条指令组成的程序段。
协程的两个原语操作:yield, resume.
- yeild:跳转到另一个代码处。此原语分别执行保存目前代码的上下文和加载要跳转到的代码的上下文。(协程将CPU控制权让给调度器)
- resume:恢复需要运行的协程。(调度器调度协程继续运行)
3.协程的切换和运行流程
协程运行的流程
先是在调度器运行,然后通过resume切换到协程A;当yield时,回到调度器运行,再通过resume切换到协程B,再通过yeild回到调度器,之后周而复始。
协程的切换
yield、resume如何实现?都是通过switch(A, B)实现协程的切换
协程中跳转实现方式:
1)setjmp/logjmp C标准方法, 使用了这种方式实现跳转。
2)ucontext,linux系统提供的的。
3)汇编,在协程中保存CPU寄存器,再将即将运行的协程的上下文寄存器mov到对应的寄存器上
汇编的方法是怎么实现切换?
协程的切换,首先在当前运行的协程中保存CPU寄存器,再将即将运行的协程的上下文寄存器mov到对应的寄存器上。
协程的结构体定义
每一个协程有自己的上下文环境
- 需要保存 CPU 的寄存器 ctx;
- 需要有子过程的回调函数 func;有子过程回调函数的参数 arg;
- 需要定义自己的栈空间stack;有自己栈空间的大小 stack_size;
- 需要定义协程的创建时间birth;定义协程当前的运行状态 status;
- 需要定当前运行状态的结点(ready_next, wait_node, sleep_node);
- 需要定义协程 id;
- 需要定义调度器的全局对象 sched。
运行状态 status,就绪(ready), 睡眠(sleep), 等待(wait)集合该采用如何数据结构来存储?
协程的核心结构体如下:
调度器结构体的定义
调度器是管理所有协程运行的组件, 协程与调度器的运行关系。
调度器的属性,需要有保存 CPU 的寄存器上下文 ctx,可以从协程运行状态yield 到调度器运行的。从协程到调度器用 yield,从调度器到协程用 resume。
以下为协程的定义
4.工作流程
创建协程
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg)
参数 1: nty_coroutine **new_co, 需要传入空的协程的对象, 这个对象是由内部创建的, 并且在函数返回的时候, 会返回一个内部创建的协程对象。
参数 2: proc_coroutine func, 协程的子过程。 当协程被调度的时候, 就会执行该函数。
参数 3: void *arg, 需要传入到新协程中的参数。
协程不存在亲属关系, 都是一致的调度关系, 接受调度器的调度。 调用 create API就会创建一个新协程,新协程就会加入到调度器的就绪队列中。
回调协程的子过程
在 create 协程后, 何时回调子过程? 何种方式回调子过程?
以 NtyCo 的实现为例,来分析这个过程。 CPU 有一个非常重要的寄存器叫做 EIP,用来存储 CPU 运行下一条指令的地址。 我们可以把回调函数的地址存储到 EIP 中,将相应的参数
存储到相应的参数寄存器中。 实现子过程调用的逻辑代码如下:
void _exec(nty_coroutine *co) {
co->func(co->arg); //子过程的回调函数
}
void nty_coroutine_init(nty_coroutine *co) {
//ctx 就是协程的上下文
co->ctx.edi = (void*)co; //设置参数
co->ctx.eip = (void*)_exec; //设置回调函数入口
//当实现上下文切换的时候,就会执行入口函数_exec , _exec 调用子过程 func
}
5.应用案例
nty_server.c
#include <malloc.h>
#include "nty_coroutine.h"
#include <arpa/inet.h>
#include <string.h>
#include <sys/poll.h>
#define MAX_CLIENT_NUM 1000000
#define TIME_SUB_MS(tv1, tv2) ((tv1.tv_sec - tv2.tv_sec) * 1000 + (tv1.tv_usec - tv2.tv_usec) / 1000)
void server_reader(void *arg) {
int fd = *(int *) arg;
int ret = 0;
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN;
while (1) {
char buf[1024] = {0};
ret = nty_recv(fd, buf, 1024, 0);
if (ret > 0) {
if (fd > MAX_CLIENT_NUM) {
printf("100w connect\n");
assert(0);
}
ret = nty_send(fd, buf, strlen(buf), 0);
if (ret == -1) {
nty_close(fd);
break;
}
}
else if (ret == 0) {
nty_close(fd);
break;
}
}
}
void server(void *arg) {
unsigned short port = *(unsigned short *) arg;
free(arg);
int fd = nty_socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return;
struct sockaddr_in local, remote;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
bind(fd, (struct sockaddr *) &local, sizeof(struct sockaddr_in));
listen(fd, 128);
printf("listen port : %d\n", port);
struct timeval tv_begin;
gettimeofday(&tv_begin, NULL);
while (1) {
socklen_t len = sizeof(struct sockaddr_in);
int cli_fd = nty_accept(fd, (struct sockaddr *) &remote, &len);
if (cli_fd % 1000 == 999) {
struct timeval tv_cur;
memcpy(&tv_cur, &tv_begin, sizeof(struct timeval));
gettimeofday(&tv_begin, NULL);
int time_used = TIME_SUB_MS(tv_begin, tv_cur);
printf("client fd : %d, time_used: %d\n", cli_fd, time_used);
}
nty_coroutine *read_co;
nty_coroutine_create(&read_co, server_reader, &cli_fd);
}
}
int main(int argc, char *argv[]) {
nty_coroutine *co = NULL;
int i = 0;
unsigned short base_port = 8080;
for (i = 0; i < 100; i++) {
unsigned short *port = calloc(1, sizeof(unsigned short));
*port = base_port + i;
nty_coroutine_create(&co, server, port);
}
nty_schedule_run(); //run
return 0;
}