【协程】协程的设计原理

一、协程的起源

  1. 为什么会有协程?
    对于响应式服务器,所有的客户端的操作驱动都是来源于这个大循环。来源于
    epoll_wait 的反馈结果。
while (1) {
   
    
    
	 int nready = epoll_wait(epfd, events, EVENT_SIZE, -1);
	 for (i = 0;i < nready;i ++) {
   
    
    
	 	int sockfd = events[i].data.fd;
	 	if (sockfd == listenfd) {
   
    
    
	 		int connfd = accept(listenfd, xxx, xxxx);
 
	 		setnonblock(connfd);
	 		ev.events = EPOLLIN | EPOLLET;
	 		ev.data.fd = connfd;
	 		epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
 		} else {
   
    
    
			handle(sockfd);
 		}
 	} 
}

对于服务器处理百万计的 IO。Handle(sockfd)实现方式有两种。

Handle方式一 ( IO同步操作)
handle(sockfd)函数内部对 sockfd 进行读写动作。代码如下

int handle(int sockfd) {
   
    
    
	recv(sockfd, rbuffer, length, 0);
	parser_proto(rbuffer, length);
	send(sockfd, sbuffer, length, 0);
}

handle 的 io 操作(send,recv)与 epoll_wait 是在同一个处理流程里面的。这就是 IO 同步操作。

优点:

1. sockfd 管理方便。
2. 操作逻辑清晰。

缺点:

 1. 服务器程序依赖 epoll_wait 的循环响应速度慢。
 2. 程序性能差

Handle方式二 (IO异步线程池操作)
handle(sockfd)函数内部将 sockfd 的操作,push 到线程池中,代码如下:

int thread_cb(int sockfd) {
   
    
    
	// 此函数是在线程池创建的线程中运行。
	// 与 handle 不在一个线程上下文中运行
	recv(sockfd, rbuffer, length, 0);
	parser_proto(rbuffer, length);
	send(sockfd, sbuffer, length, 0);
}
int handle(int sockfd) {
   
    
    
	//此函数在主线程 main_thread 中运行
	//在此处之前,确保线程池已经启动。
	push_thread(sockfd, thread_cb); //将 sockfd 放到其他线程中运行。
}

Handle 函数是将 sockfd 处理方式放到另一个已经其他的线程中运行,如此做法,将 io 操作(recv,send)与 epoll_wait 不在一个处理流程里面,使得 io操作(recv,send)与epoll_wait 实现解耦。这就叫做 IO 异步操作。

优点:

1. 子模块好规划。
2. 程序性能高。

缺点:

正因为子模块好规划,使得模块之间的 sockfd 的管理异常麻烦。每一个子线程都需要管理好 sockfd,避免在 IO 操作的时候,sockfd 出现关闭或其他异常。

2. 协程解决了什么问题?
实验证明,IO 同步操作,程序响应慢,IO 异步操作,程序响应快。

IO 异步操作与 IO 同步操作对比:

对比项 IO同步操作 IO异步操作
Socket管理 管理方便 多个线程共同管理
代码逻辑 程序整体逻辑清晰 子模块逻辑清晰
程序性能 响应时间长,性能差 响应时间短,性能好

有没有一种方式,有异步性能,同步的代码逻辑。来方便编程人员对 IO 操作的组件呢? 有,采用一种轻量级的协程来实现。在每次 send 或者 recv 之前进行切换,再由调度器来处理 epoll_wait 的流程。

总结
协程解决的问题是:通过异步IO的方式,实现看起来是同步IO的代码逻辑。

3. 协程如何使用?与线程使用有何区别?
在做网络 IO 编程的时候,有一个非常理想的情况,就是每次 accept 返回的时候,就为新来的客户端分配一个线程,这样一个客户端对应一个线程。就不会有多个线程共用一个sockfd。每请求每线程的方式,并且代码逻辑非常易读。但是这只是理想,线程创建代价,但是调度代价就非常大了。

先来看一下每请求每线程的代码如下:

while(1) {
   
    
    
	socklen_t len = sizeof(struct sockaddr_in);
	int clientfd = accept(sockfd, (struct sockaddr*)&remote, &len);
	pthread_t thread_id;
	pthread_create(&thread_id, NULL, client_cb, &clientfd);
}

如果我们有协程,我们就可以这样实现。参考代码如下:

while (1) {
   
    
    
	socklen_t len = sizeof(struct sockaddr_in);
	int cli_fd = nty_accept(fd, (struct sockaddr*)&remote, &len);
	nty_coroutine *read_co;
	nty_coroutine_create(&read_co, server_reader, &cli_fd);
}

线程的 API 思维来使用协程,函数调用的性能来测试协程。

4.协程可以用在什么地方?
协程可以使用的地方如:

  • 文件操作
  • mysql的操作
  • 网络io
  • redis的操作
    这些需要等待IO完成耗时的地方,通过协程变成异步非阻塞IO,在等待IO事件完成时,把协程让出给协程调度器,由协程调度器决定调度某个已经完成IO的协程执行。

这样的好处是:

  • 代码看起来是同步阻塞IO的逻辑,比较好理解,但实际上是异步非阻塞IO。(注:如果操作没有IO,那么用协程的意义不大。)
  • 切换协程执行比内核切换线程的代价小得多。一个线程运行期间,其中某个协程阻塞了,马上切换另一个协程执行(由协程调度器决定切换到哪一个协程执行),切换代价很小。如果是直接调用系统阻塞API,那么当前线程就会阻塞,内核切换线程,切换代价很大。因此协程有必要对系统阻塞API进行一层封装。

所有可能引起阻塞的系统API,都可以通过协程调度器封装一层API,使得本来是同步阻塞的IO操作,通过我们封装好的协程调度器API把它转变成异步非阻塞IO的操作。

不需要封装的API(不可能导致阻塞)如:socket、close、fcntl、setsockopt、getsockopt、listen等。

需要封装的API(可能导致阻塞)如:系统API中的read、write、connect、accept、send、sendto、recv、recvfrom、sleep等。

connect 方法在建立连接时,会阻塞直到连接成功建立或者发生错误。
accept 方法在等待客户端连接时,会阻塞直到有客户端连接请求到达或者出现错误。
send/sendto 方法在发送数据时,会阻塞等待直到数据成功发送完毕或者发生错误。
recv/recvfrom 方法在接收数据时,会阻塞等待直到有数据到达或者发生错误。
read/write 方法在读写数据时,如果没有数据可读或者缓冲区已满,会阻塞等待直到有数据可读或者缓冲区有空闲空间。

封装API示例:

nty_recv(){
   
    
    
	//添加到epoll管理。原理就是利用epoll管理所有的IO
	epoll_ctl(add)
	//直接让出CPU
	yield();
	//执行下面代码说明已经被epoll捕捉到sockfd有数据了,并且协程调度器主动resume了当前协程
	//首先移除epoll管理
	epoll_ctl(del)
	//可以直接调用系统API读数据了,因为一定有数据可以读,不会出现阻塞或者无数据可读情况
	recv(sockfd);
}

如果想要封装系统API中的read、write、connect、accept、send、sendto、recv、recvfrom、sleep等,成为自己的同名API,可参考如下代码:

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>

#include <fcntl.h>
#include <sys/select.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>

#include <mysql.h>

//原生系统API
typedef int(*connect_t)(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
connect_t connect_f;

typedef int(*accept_t)(int sockfd, struct sockaddr *addr

猜你喜欢

转载自blog.csdn.net/cangqiong_xiamen/article/details/139445514