协程学习一:nty协程的设计原理及效率分析

1.协程的库

1.golang的libgo(使用非常简单,C++实现)
2.腾讯的libco(非常好用的,C++实现)
3.ntyco(纯C的代码写)–wangbojing
4.今一些具备协程语义的语言,比较重量级的如C#、erlang、golang,以及轻量级的python、lua、javascript、ruby,还有函数式的scala、scheme等

2.为什么会有协程?

1.协程就是轻量级线程,而普通线程切换成本高,性能类似io异步操作
2.异步化,起初人们喜欢同步编程,然后发现有一堆线程因为I/O卡在那里,并发上不去,资源严重浪费
3.线程安全,任何挂起的函数可以在主线程调用。
4.协程作为有异步性能,同步的代码逻辑。来方便编程人员对 IO 操作的
组件
5.总结
在这里插入图片描述

3.协程解决了什么问题?

1)同步的方式实现异步的效率,建立1000条连接,同步需要5400毫秒,异步需要850毫秒
2)同步代码(epoll_wait和send、wait在同意不流程里面)
在这里插入图片描述
3)异步代码(epoll_wait检测io可读写之后放到另外一个线程里面)
在这里插入图片描述
4)异步io与io异步操作
io异步操作概念:就是上面异步的代码与操作讲解(多线程异步)
异步io概念:例如AIO,纯异步,有数据直接回调数据

4.协程有栈和无栈的区别

1)有栈:每一个协程,有独立的栈,但是有栈的性能更高
优点:实现容易,性能更高
缺点:栈利用率不好
特点:Stackfull是真正的基于栈的重入,可以从某个嵌套的调用点上恢复执行。Stackfull协程在切换的时候,需要把当前的栈保存起来,以便在恢复的时候再次恢复执行,而stackless的则不需要。

2)无栈:共享栈(stackless),对内存利用率更高,但是更加复杂
优点:栈利用率高
缺点:性能可能比前者低
特点:Stackless类型不保存调用栈以及寄存器等信息,不属于真正的重入,因此一些局部变量都是无法使用的。

5.协程的切换怎么实现(Longjmp/setjmp,utext,汇编三种方法)

1.声明(汇编实现)
在这里插入图片描述
2.实际让出CPU资源的代码(yield)
在这里插入图片描述

int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);

#ifdef __i386__
__asm__ (
"    .text                                  \n"
"    .p2align 2,,3                          \n"
".globl _switch                             \n"
"_switch:                                   \n"
"__switch:                                  \n"
"movl 8(%esp), %edx      # fs->%edx         \n"
"movl %esp, 0(%edx)      # save esp         \n"
"movl %ebp, 4(%edx)      # save ebp         \n"
"movl (%esp), %eax       # save eip         \n"
"movl %eax, 8(%edx)                         \n"
"movl %ebx, 12(%edx)     # save ebx,esi,edi \n"
"movl %esi, 16(%edx)                        \n"
"movl %edi, 20(%edx)                        \n"
"movl 4(%esp), %edx      # ts->%edx         \n"
"movl 20(%edx), %edi     # restore ebx,esi,edi      \n"
"movl 16(%edx), %esi                                \n"
"movl 12(%edx), %ebx                                \n"
"movl 0(%edx), %esp      # restore esp              \n"
"movl 4(%edx), %ebp      # restore ebp              \n"
"movl 8(%edx), %eax      # restore eip              \n"
"movl %eax, (%esp)                                  \n"
"ret                                                \n"
);
#elif defined(__x86_64__)

__asm__ (
"    .text                                  \n"
"       .p2align 4,,15                                   \n"
".globl _switch                                          \n"
".globl __switch                                         \n"
"_switch:                                                \n"
"__switch:                                               \n"
"       movq %rsp, 0(%rsi)      # save stack_pointer     \n"
"       movq %rbp, 8(%rsi)      # save frame_pointer     \n"
"       movq (%rsp), %rax       # save insn_pointer      \n"
"       movq %rax, 16(%rsi)                              \n"
"       movq %rbx, 24(%rsi)     # save rbx,r12-r15       \n"
"       movq %r12, 32(%rsi)                              \n"
"       movq %r13, 40(%rsi)                              \n"
"       movq %r14, 48(%rsi)                              \n"
"       movq %r15, 56(%rsi)                              \n"
"       movq 56(%rdi), %r15                              \n"
"       movq 48(%rdi), %r14                              \n"
"       movq 40(%rdi), %r13     # restore rbx,r12-r15    \n"
"       movq 32(%rdi), %r12                              \n"
"       movq 24(%rdi), %rbx                              \n"
"       movq 8(%rdi), %rbp      # restore frame_pointer  \n"
"       movq 0(%rdi), %rsp      # restore stack_pointer  \n"
"       movq 16(%rdi), %rax     # restore insn_pointer   \n"
"       movq %rax, (%rsp)                                \n"
"       ret                                              \n"
);
#endif

6.协程的启动

1.协程的创建
在这里插入图片描述

2.创建后如何加入到就绪队列
3.协程入口函数怎么调用
eip指针指向入口函数,比如入口函数func的地址指向eip
栈指针esp–>的首地址保存起来,放在数组也是ok的(eip指向栈)
在这里插入图片描述

7.协程定义

1.上下文context,用来切换用,用来存cpu寄存器的值
2.每个协程的栈空间stack,栈就是协程内部进行函数调用用来压栈的栈指针要用到
3.协程栈的大小size
4.协程的入口函数func
5.func的入口函数arg
6.协程处于wait\sleep\ready的状态,这三个都是集合元素,可分别为红黑树、红黑树、队列
7.exit退出
在这里插入图片描述

8、协程调度器是怎么实现(协程调度器schedule)

1.找到当前协程运行的协程curr_coroutine
2.当前用到的共享栈,共享栈的化,栈就放在调度器里面,不放在协程里面
3.调度器用一个while一直在运行,三个集合:①ready ,②wait,③io_wait部分
4.具体
①判断sleep是否到期
②就绪队列
③epoll里面这部分
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

补充:
①yield底层调用swicth交换两个协程
②resume是恢复协程的运行,yield是协程让出cpu执行权限

9.协程实现的工作流程

  • 1.创建协程

当我们需要异步调用的时候,我们会创建一个协程。比如 accept 返回一个新的sockfd,创建一个客户端处理的子过程。再比如需要监听多个端口的时候,创建一个 server的子过程,这样多个端口同时工作的,是符合微服务的架构的。
创建协程的时候,进行了如何的工作?创建 API 如下:

// coroutine --> 
// create 
//
int nty_coroutine_create(nty_coroutine **new_co, proc_coroutine func, void *arg) {

	assert(pthread_once(&sched_key_once, nty_coroutine_sched_key_creator) == 0);
	nty_schedule *sched = nty_coroutine_get_sched();

	if (sched == NULL) {
		nty_schedule_create(0);
		
		sched = nty_coroutine_get_sched();
		if (sched == NULL) {
			printf("Failed to create scheduler\n");
			return -1;
		}
	}

	nty_coroutine *co = calloc(1, sizeof(nty_coroutine));
	if (co == NULL) {
		printf("Failed to allocate memory for new coroutine\n");
		return -2;
	}

	int ret = posix_memalign(&co->stack, getpagesize(), sched->stack_size);
	if (ret) {
		printf("Failed to allocate stack for new coroutine\n");
		free(co);
		return -3;
	}

	co->sched = sched;
	co->stack_size = sched->stack_size;
	co->status = BIT(NTY_COROUTINE_STATUS_NEW); //
	co->id = sched->spawned_coroutines ++;
	co->func = func;
#if CANCEL_FD_WAIT_UINT64
	co->fd = -1;
	co->events = 0;
#else
	co->fd_wait = -1;
#endif
	co->arg = arg;
	co->birth = nty_coroutine_usec_now();
	*new_co = co;

	TAILQ_INSERT_TAIL(&co->sched->ready, co, ready_next);

	return 0;
}

参数 1:nty_coroutine **new_co,需要传入空的协程的对象,这个对象是由内部
创建的,并且在函数返回的时候,会返回一个内部创建的协程对象。
参数 2:proc_coroutine func,协程的子过程。当协程被调度的时候,就会执行该
函数。
参数 3:void *arg,需要传入到新协程中的参数

  • 2.io异步操作

在协程的上下文 IO 异步操作(nty_recv,nty_send)函数,步骤如下:

    1. 将 sockfd 添加到 epoll 管理中。
    1. 进行上下文环境切换,由协程上下文 yield 到调度器的上下文。
    1. 调度器获取下一个协程上下文。Resume 新的协程IO 异步操作的上下文切换的时序图如下:
      在这里插入图片描述
//recv 
// add epoll first
//
ssize_t nty_recv(int fd, void *buf, size_t len, int flags) {
	
	struct pollfd fds;
	fds.fd = fd;
	fds.events = POLLIN | POLLERR | POLLHUP;

	nty_poll_inner(&fds, 1, 1);	//nty_poll_inner判断io是否准备就绪

	int ret = recv(fd, buf, len, flags);
	if (ret < 0) {
		//if (errno == EAGAIN) return ret;
		if (errno == ECONNRESET) return -1;
		//printf("recv error : %d, ret : %d\n", errno, ret);
		
	}
	return ret;
}

ssize_t nty_send(int fd, const void *buf, size_t len, int flags) {
	
	int sent = 0;

	int ret = send(fd, ((char*)buf)+sent, len-sent, flags);
	if (ret == 0) return ret;
	if (ret > 0) sent += ret;

	while (sent < len) {
		struct pollfd fds;
		fds.fd = fd;
		fds.events = POLLOUT | POLLERR | POLLHUP;

		nty_poll_inner(&fds, 1, 1);			//nty_poll_inner判断io是否准备就绪
		ret = send(fd, ((char*)buf)+sent, len-sent, flags);
		//printf("send --> len : %d\n", ret);
		if (ret <= 0) {			
			break;
		}
		sent += ret;
	}

	if (ret <= 0 && sent == 0) return ret;
	
	return sent;
}


/*
 * nty_poll_inner --> 1. sockfd--> epoll, 2 yield, 3. epoll x sockfd
 * fds : 
 */

static int nty_poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) {

	//1.如果超时时间为0,直接调用poll
	if (timeout == 0)
	{
		return poll(fds, nfds, timeout);
	}
	if (timeout < 0)
	{
		timeout = INT_MAX;
	}

	nty_schedule *sched = nty_coroutine_get_sched();
	nty_coroutine *co = sched->curr_thread;
	
	int i = 0;
	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);
		ev.data.fd = fds[i].fd;
		//2.如果io没准备就绪,就把io加入epoll里面
		//(这个epoll是schedule的epoll,这个epoll是全局的,所有协程都公用一个,他是管理所有io的)
		epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev);

		co->events = fds[i].events;
		nty_schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout);
	}
	//3.没就绪的io加入epoll之后,就是yield
	nty_coroutine_yield(co);  //1  

	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);
		ev.data.fd = fds[i].fd;
		epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev);

		nty_schedule_desched_wait(fds[i].fd);
	}

	return nfds;
}
  • 3.协程子过程回调

在 create 协程后,何时回调子过程?何种方式回调子过程?
首先来回顾一下 x86_64 寄存器的相关知识。汇编与寄存器相关知识还会在《协程的实现之切换》继续深入探讨的。x86_64 的寄存器有 16 个 64 位寄存器,分别是:%rax, %rbx,%rcx, %esi, %edi, %rbp, %rsp, %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15。%rax 作为函数返回值使用的。 %rsp 栈指针寄存器,指向栈顶%rdi, %rsi, %rdx, %rcx, %r8, %r9 用作函数参数,依次对应第 1 参数,第 2 参数。。。%rbx, %rbp, %r12, %r13, %r14, %r15 用作数据存储,遵循调用者使用规则,换句话说,就是随便用。调用子函数之前要备份它,以防它被修改%r10, %r11 用作数据存储,就是使用前要先保存原值

以 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
}

//完整代码
static void _exec(void *lt) {
#if defined(__lvm__) && defined(__x86_64__)
	__asm__("movq 16(%%rbp), %[lt]" : [lt] "=r" (lt));
#endif

	nty_coroutine *co = (nty_coroutine*)lt;
	co->func(co->arg); //
	co->status |= (BIT(NTY_COROUTINE_STATUS_EXITED) | BIT(NTY_COROUTINE_STATUS_FDEOF) 
		| BIT(NTY_COROUTINE_STATUS_DETACH));
	
#if 1
	nty_coroutine_yield(co);
#else
	co->ops = 0;
	_switch(&co->sched->ctx, &co->ctx);
#endif
}
static void nty_coroutine_init(nty_coroutine *co) {

	void **stack = (void **)(co->stack + co->stack_size);

	stack[-3] = NULL;
	stack[-2] = (void *)co;

	co->ctx.esp = (void*)stack - (4 * sizeof(void*));
	co->ctx.ebp = (void*)stack - (3 * sizeof(void*));
	co->ctx.eip = (void*)_exec;
	co->status = BIT(NTY_COROUTINE_STATUS_READY);
	
}

上面的协程初始化是在resume里面做的事

int nty_coroutine_resume(nty_coroutine *co) {
	
	if (co->status & BIT(NTY_COROUTINE_STATUS_NEW)) {
		nty_coroutine_init(co);
	}

	nty_schedule *sched = nty_coroutine_get_sched();
	sched->curr_thread = co;
	_switch(&co->ctx, &co->sched->ctx);
	sched->curr_thread = NULL;

	nty_coroutine_madvise(co);
#if 1
	if (co->status & BIT(NTY_COROUTINE_STATUS_EXITED)) {
		if (co->status & BIT(NTY_COROUTINE_STATUS_DETACH)) {
			printf("nty_coroutine_resume --> \n");
			nty_coroutine_free(co);
		}
		return -1;
	} 
#endif
	return 0;
}

调度器代码

//调度器代码
void nty_schedule_run(void) {

	nty_schedule *sched = nty_coroutine_get_sched();
	if (sched == NULL) return ;

	while (!nty_schedule_isdone(sched)) {
		
		// 1. expired --> sleep rbtree(sleep的情况是非常少的)
		//key就是时间,value就是协程(若时间相同插入失败,那就把时间增加一点再次插)nty_schedule_sched_sleepdown
		//遍历睡眠集合,将满足条件的加入到ready
		//找到哪些时间到期了,resume恢复他的运行
		nty_coroutine *expired = NULL; //
		while ((expired = nty_schedule_expired(sched)) != NULL) {
			nty_coroutine_resume(expired);
		}

		// 2. ready queue就绪队列(为了解决刚开始创建的状态)
		//遍历等待集合,将满足添加的加入到ready
		nty_coroutine *last_co_ready = TAILQ_LAST(&sched->ready, _nty_coroutine_queue);
		while (!TAILQ_EMPTY(&sched->ready)) {
			nty_coroutine *co = TAILQ_FIRST(&sched->ready);
			TAILQ_REMOVE(&co->sched->ready, co, ready_next);

			if (co->status & BIT(NTY_COROUTINE_STATUS_FDEOF)) {
				nty_coroutine_free(co);
				break;
			}
			//拿出第一个节点开始运行
			nty_coroutine_resume(co);
			if (co == last_co_ready) break;
		}

		// 3. wait rbtree(io等待,非常常见)
		//使用resume恢复ready的协程运行权
		nty_schedule_epoll(sched);
		while (sched->num_new_events) {
			int idx = --sched->num_new_events;
			struct epoll_event *ev = sched->eventlist+idx;
			
			int fd = ev->data.fd;
			int is_eof = ev->events & EPOLLHUP;
			if (is_eof) errno = ECONNRESET;

			nty_coroutine *co = nty_schedule_search_wait(fd);
			if (co != NULL) {
				if (is_eof) {
					co->status |= BIT(NTY_COROUTINE_STATUS_FDEOF);
				}
				nty_coroutine_resume(co);
			}

			is_eof = 0;
		}
	}

	nty_schedule_free(sched);
	
	return ;
}

10.如何实现yield和resume(用户态切换,比线程切换开销小很多)

1.longjmp和setjmp,longjmp跳过去,setjmp在跳回来
2.linux接口ucontex
3.汇编代码实现跳转

11.异步的四个步骤

1.客户端先初始化上下文,也就是初始化epoll
2,与初始化对应的就是结束io
3.对应的回调处理函数callback接受mysql返回的数据
4.commit一次一次提交具体的事务

12.协程一直不让出(协程是为了解决io等待挂起的问题)

  • 注意:

若协程本身没有io操作的话,那么协程的意义不大;那么用线程处理意义是一样的

13.协程调度器怎么跑起来(重点)

main主函数

开始创建协程并进入协程的执行函数server

int main(int argc, char *argv[]) {
	//1.创建协程
	nty_coroutine *co = NULL;

	//2.监听100个端口
	int i = 0;
	unsigned short base_port = 8888;//从8888端口开始,监听100个端口
	for (i = 0;i < 100;i ++) {
		unsigned short *port = calloc(1, sizeof(unsigned short));
		*port = base_port + i;
		//port是协程的参数
		//server是协程的运行函数
		nty_coroutine_create(&co, server, port); no run
	}

	//3.开始调度器调度
	nty_schedule_run(); //run

	return 0;
}

协程执行函数,流程在代码中

void server(void *arg) {

	//1.创建套接字开始监听
	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, 20);
	printf("listen port : %d\n", port);

	//2.获取当前时间	
	struct timeval tv_begin;
	gettimeofday(&tv_begin, NULL);

	//3.利用监听套接字获得客户端的套接字
	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);
		}
		//printf("new client comming\n");

		nty_coroutine *read_co;
		nty_coroutine_create(&read_co, server_reader, &cli_fd);

	}
}

nty_accept的nty_poll_inner判断协程io是否就绪

//nty_accept 
//return failed == -1, success > 0

int nty_accept(int fd, struct sockaddr *addr, socklen_t *len) {
	int sockfd = -1;
	int timeout = 1;
	nty_coroutine *co = nty_coroutine_get_sched()->curr_thread;
	
	while (1) {
		struct pollfd fds;
		fds.fd = fd;
		fds.events = POLLIN | POLLERR | POLLHUP;
		nty_poll_inner(&fds, 1, timeout);					//nty_poll_inner判断io是否准备就绪

		sockfd = accept(fd, addr, len);
		if (sockfd < 0) {
			if (errno == EAGAIN) {
				continue;
			} else if (errno == ECONNABORTED) {
				printf("accept : ECONNABORTED\n");
				
			} else if (errno == EMFILE || errno == ENFILE) {
				printf("accept : EMFILE || ENFILE\n");
			}
			return -1;
		} else {
			break;
		}
	}

	int ret = fcntl(sockfd, F_SETFL, O_NONBLOCK);
	if (ret == -1) {
		close(sockfd);
		return -1;
	}
	int reuse = 1;
	setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
	
	return sockfd;
}

nty_poll_inner

若超时为0直接调用poll,否则加入epoll中,然后yield挂起

/*
 * nty_poll_inner --> 1. sockfd--> epoll, 2 yield, 3. epoll x sockfd
 * fds : 
 */

static int nty_poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) {

	//1.如果超时时间为0,直接调用poll
	if (timeout == 0)
	{
		return poll(fds, nfds, timeout);
	}
	if (timeout < 0)
	{
		timeout = INT_MAX;
	}

	nty_schedule *sched = nty_coroutine_get_sched();
	nty_coroutine *co = sched->curr_thread;
	
	int i = 0;
	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);
		ev.data.fd = fds[i].fd;
		//2.如果io没准备就绪,就把io加入epoll里面
		//(这个epoll是schedule的epoll,这个epoll是全局的,所有协程都公用一个,他是管理所有io的)
		epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev);

		co->events = fds[i].events;
		nty_schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout);
	}
	//3.没就绪的io加入epoll之后,就是yield
	nty_coroutine_yield(co);  //1  

	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);
		ev.data.fd = fds[i].fd;
		epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev);

		nty_schedule_desched_wait(fds[i].fd);
	}

	return nfds;
}

main函数的调度器运行起来

void nty_schedule_run(void) {

	nty_schedule *sched = nty_coroutine_get_sched();
	if (sched == NULL) return ;

	while (!nty_schedule_isdone(sched)) {
		
		// 1. expired --> sleep rbtree(sleep的情况是非常少的)
		//key就是时间,value就是协程(若时间相同插入失败,那就把时间增加一点再次插)nty_schedule_sched_sleepdown
		//遍历睡眠集合,将满足条件的加入到ready
		//找到哪些时间到期了,resume恢复他的运行
		nty_coroutine *expired = NULL; //
		while ((expired = nty_schedule_expired(sched)) != NULL) {
			nty_coroutine_resume(expired);
		}

		// 2. ready queue就绪队列(为了解决刚开始创建的状态)
		//遍历等待集合,将满足添加的加入到ready
		nty_coroutine *last_co_ready = TAILQ_LAST(&sched->ready, _nty_coroutine_queue);
		while (!TAILQ_EMPTY(&sched->ready)) {
			nty_coroutine *co = TAILQ_FIRST(&sched->ready);
			TAILQ_REMOVE(&co->sched->ready, co, ready_next);

			if (co->status & BIT(NTY_COROUTINE_STATUS_FDEOF)) {
				nty_coroutine_free(co);
				break;
			}
			//拿出第一个节点开始运行
			nty_coroutine_resume(co);
			if (co == last_co_ready) break;
		}

		// 3. wait rbtree(io等待,非常常见)
		//使用resume恢复ready的协程运行权
		nty_schedule_epoll(sched);
		while (sched->num_new_events) {
			int idx = --sched->num_new_events;
			struct epoll_event *ev = sched->eventlist+idx;
			
			int fd = ev->data.fd;
			int is_eof = ev->events & EPOLLHUP;
			if (is_eof) errno = ECONNRESET;

			nty_coroutine *co = nty_schedule_search_wait(fd);
			if (co != NULL) {
				if (is_eof) {
					co->status |= BIT(NTY_COROUTINE_STATUS_FDEOF);
				}
				nty_coroutine_resume(co);
			}

			is_eof = 0;
		}
	}

	nty_schedule_free(sched);
	
	return ;
}

14.多核的问题(进程都是单线程的)

1.多进程,代码改动的地方不多,几乎不需要改动,比如说ntycp是多进程的
2.多线程,比多进程复杂很多(可以每个线程绑定CPU),需要对调度器进行加锁(怎么加?举例:在nty_schedule_expired这个函数里面对树进行加锁,在红黑树里面找出一个节点需要加锁。锁定义在调度器里面)
3.x86多核指令可以实现

15.协程实现的需要提供给外接的接口

1)协程的创建
2)启动调度器schedule_loop()
3)sleep
4)read封装成nty_read类似的
5)socket()和listen()\close()、fcntl()\setsocketopt()\getsocketopt()没必要封装成异步的
6)accept()\connect()\send()\write()\recv()\read()
sendto()\recvfrom()
会引起阻塞状态的接口,需要封装成异步的,都是下面代码的流程

nty_read()		//这就成了异步的read
{
	fd->epoll; //1.可以先把fd都放到epoll里面
	yield;	   //2.然后再yield让出,先让其他协程read,等待他yield让出再读自己的read
	read();	   //3.在调用系统的read函数
}

16.非阻塞与协程

1.若接口改为非阻塞,性能和协程一样
2.但是协程更加具有可读性

17.协程的实现方案

  • 1.一种是setjmp/longjmp
  • 2.另一种是 ucontext 组件,相对比第一种,它们内部(当然是用汇编语言)实现了协程的上下文切换,相较之下前者在应用上会产生相当的不确定性(比如不好封装,具体说明参考联机文档),所以后者应用更广泛一些,网上绝大多数 C 协程库也是基于 ucontext 组件实现的。

链接一
链接二

  • 3.汇编代码实现
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值