为什么我们要用Reactor和Proactor?

一、服务器如何与客户端通信

1. 多线程

  • 每个连接都用一个线程来处理,连接关闭,线程也被销毁。
  • 当一个连接对应一个线程时,线程一般按【read->业务流程->send】的处理流程,如果当前连接没有数据可读,线程在read上阻塞,此时不影响其他线程
  • 缺点:反复创建、销毁线程,非常浪费资源;无法创建成千上万的线程去满足连接。

2. 线程池

  • 创建一个线程池,将连接分配给线程,一个线程可以处理多个连接的任务。
  • 一个线程在处理某个连接的read时,如果没数据可读,就会阻塞,无法继续处理其他连接的业务。
  • 一个处理办法就是将socket改为非阻塞,然后不断轮询调用read,判断是否有数据。轮询要消耗CPU,如果一个线程处理的连接越多,轮询效率就很低。

3. IO多路复用

  • 上面的问题在于,线程并不知道当前连接是否有数据可读,从而需要每次通过 read 去试探。
  • I/O 多路复用技术会用一个系统调用函数(select、poll、epoll)来监听我们所有关心的连接,也就说可以在一个监控线程里面监控很多的连接。
    • 如果没有事件发生,线程只需阻塞在这个系统调用和当前的监听线程,而无需像前面的线程池方案那样轮训调用 read 操作来判断是否有数据。
    • 如果有事件发生,内核会返回产生了事件的连接,线程就会从阻塞状态返回,然后在用户态中再处理这些连接对应的业务即可

二、Reactor模式

  • 概念

    • 对IO多路复用进行封装,并将这种模式称为Reactor模式。
    • Reactor模式下,主线程只复制fd监听,工作线程完成【read -> 业务处理 -> send】
  • 反应堆:对事件反应,来了一个事件,Reactor就有相应的反应。

  • 主流模型

    • 单 Reactor 单进程 / 线程
    • 单 Reactor 多线程 / 进程
    • 多 Reactor 多进程 / 线程

1. 单 Reactor 单进程 / 线程

在这里插入图片描述

  • 流程

    • Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
    • 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
    • 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
    • Handler 对象通过【 read -> 业务处理 -> send 】的流程来完成完整的业务流程。
  • 示例

    while(true)
    {
    	int rt = epoll_wait(lfd, evs);
    	for(int i = 0; i<rt; i++)
    	{
    		if(evs.fd = lfd && evs.event == READ)
    		{
    			//建立新的连接
    			int cfd = accept();
    			//给cfd加事件到epoll
    			epoll_ctl(cfd, READ|WRITE);
    		}
    		else if(evs.event == READ)
    		{
    			recv(evs.fd,);
    		}
    		else if(evs.event == WRITE)
    		{
    			send(evs.fd);
    		}
    	}
    }
    
    
  • 缺点

    • 因为只有一个进程,无法充分利用 多核 CPU 的性能;
    • Handler 对象在业务处理时,整个进程是无法处理其他连接的事件的,如果业务处理耗时比较长,那么就造成响应的延迟
    • 不适用计算机密集型的场景,只适用于业务处理非常快速的场景
  • 应用

    • Redis 采用【单Reactor单进程】模型,因为 Redis 业务处理主要是在内存中完成,操作的速度是很快的,性能瓶颈不在 CPU 上,所以 Redis 对于命令的处理是单进程的方案。

2. 单 Reactor 多线程

在这里插入图片描述

  • 流程

    • Reactor 对象通过 select (IO 多路复用接口) 监听事件,收到事件后通过 dispatch 进行分发,具体分发给 Acceptor 对象还是 Handler 对象,还要看收到的事件类型;
    • 如果是连接建立的事件,则交由 Acceptor 对象进行处理,Acceptor 对象会通过 accept 方法 获取连接,并创建一个 Handler 对象来处理后续的响应事件;
    • 如果不是连接建立事件, 则交由当前连接对应的 Handler 对象来进行响应;
    • Handler 对象不再负责业务处理,只负责数据的接收和发送,Handler 对象通过 read 读取到数据后,会将数据发给子线程里的 Processor 对象进行业务处理
    • 子线程里的 Processor 对象就进行业务处理,处理完后,将结果发给主线程中的 Handler 对象,接着由 Handler 通过 send 方法将响应结果发送给 client;
  • 示例

    thread_pool;	//线程池
    work_list;		//任务队列
    
    
    
    //线程池中线程的执行函数
    void run()
    {
    	wait(work_list is empty);
    	for(work_list)
    	{
    		fd = work_list.pop();
    		if(fd == READ)
    		{
    			recv(fd);
    		}
    		else if(fd == WRITE)
    		{
    			send(fd);
    		}
    	}
    }
    
    //主线程负责监听所有的fd
    void main_thread()
    {
    	while(true)
    	{
    		int rt = epoll_wait(lfd, evs);
    		for(int i = 0; i<rt; i++)
    		{
    			if(evs.fd = lfd && evs.event == READ)
    			{
    				pthread_create_thread(do_accept);	
    			}
    			else 
    			{
    				//主线程只复制监听,工作线程【read -> 业务处理 -> send 】
    				work_list.push(evs.fd);
    			}
    		}
    	}
    }
    
    
  • 缺点

    • 存在并发问题。
  • 单Reactor的缺点

    • 因为一个 Reactor 对象(都放在一个线程里监听了)承担所有事件的监听和响应,而且只在主线程中运行,在面对瞬间高并发的场景时,容易成为性能的瓶颈的地方。

3. 多 Reactor 多进程 / 线程

在这里插入图片描述

  • 流程

    • 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;
    • 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。
    • 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。
    • Handler 对象通过【 read -> 业务处理 -> send 】的流程来完成完整的业务流程。
  • 优点

    • 主线程和子线程分工明确,主线程只负责接收新连接子线程负责完成后续的业务处理
    • 主线程和子线程的交互很简单,主线程只需要把新连接传给子线程,子线程无须返回数据,直接就可以在子线程将处理结果发送给客户端。

三、Proactor模式

  • 异步网络模式。关于阻塞,非阻塞,同步,异步的知识,可以阅读之前的文章
  • Proactor将所有的IO操作都交给主线程和内核处理,工作线程只负责业务逻辑
  • Linux下的异步IO并不完善,但是可以使用同步IO来模拟Proactor模式
    • 主线程执行数据读写操作,读写完成后,主线程向工作线程通知这一“完成事件”,从工作线程的角度讲,它直接获得了数据的读写结果,接下来只需要直接对数据进行逻辑处理。
    • 举例
      	thread_pool;	//线程池
      	work_list;		//任务队列
      	
      	
      	
      	//线程池中线程的执行函数
      	void run()
      	{
      		wait(work_list is empty);
      		for(work_list)
      		{
      			data = work_list.pop();
      			//处理data
      			//往eoll注册写事件
      			epoll_ctl(fd, WRITE);
      		}
      	}
      	
      	//主线程负责监听所有的fd
      	void main_thread()
      	{
      		while(true)
      		{
      			int rt = epoll_wait(lfd, evs);
      			for(int i = 0; i<rt; i++)
      			{
      				if(evs.fd = lfd && evs.event == READ)
      				{
      					pthread_create_thread(do_accept);	
      				}
      				else if(evs.event == READ)		//主线程完成读写,工作线程进行逻辑处理
      				{
      					recv_data = recv(evs.fd);
      					work_list.push(recv_data);
      				} 
      				else if(evs.event == WRITE)
      				{
      					send_data;
      					send(send_data);
      				}
      			}
      		}
      	}
      
      

区别

  • Reactor是非阻塞同步网络模式,感知的是就绪可读事件。在每次监听到有事件发生,就调用系统函数(比如read)来完成数据读取。这个过程是由进程主动将socket接收缓存中的数据读到内存,是同步的,读完才能处理数据。
  • Proactor是异步网络模式,感知的是已完成的读写事件。读写操作是系统来完成的,完成后,再通知应用进程直接处理数据。一般情况下,可以使用同步IO模拟出Proactor。

参考文章
https://zhuanlan.zhihu.com/p/368089289

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值