目录
1. 前言
开始的服务端的设计是每一个连接都创建一个thread去处理,但是当请求开始变多了之后,比如有1w的request到达服务端,就会 pthread_create() 创建1w个thread,结束之后销毁线程。这样的创建,处理完然后销毁无疑是非常消耗资源的。
之后就升级,采用线程池处理请求,提前在一个队列中创建多个线程,从线程池取出线程处理。还有要注意,这个线程队列是全局共享的,为了避免多个线程竞争,要提前给这个线程加锁。
采用线程池处理主要有两个原因:1、不用频繁的创建和销毁线程。2、线程池能处理的线程有上限,可以防止无限开辟线程而将服务器资源消耗殆尽,挂掉。
线程池:https://en.wikipedia.org/wiki/Thread_pool
实现:ThreadPool/ThreadPool.h at master · progschj/ThreadPool · GitHub
其实在上面的模型是个【多线程模型】,其实还有一种【多进程模型】。主要思想是利用 fork() 函数,创建子进程。父进程要处理好自己的孩子,就是在进程退出的时候要回收好资源,否则可能变成【僵尸进程】(进程只保留了相关的信息,但是占用了进程号)。怎么回收资源呢?答案是调用【wait()】和【waitpid()】。在这个过程中,对父进程和子进程的处理是不相同的:
- 父进程:只需要管理监听的 【Socket】描述符,而不关心已经连接的【Socket】+ 子进程:只关心已经连接的【Socket】,不用管监听的【Socket】
上面两个模型,【多线程模型】缺点主要是服务端如果同时维护多个线程是不现实的,操作系统会扛不住;【多进程模型】缺点则是在多个连接同时存在的情况下进程的上下文切换所耗费的系统资源过大。这个时候更为设计一种高效的模式是当务之急,【Reactor模式】应运而生
2、主要思想
【Reactor模式】基于【select/poll/epoll】的【IO多路复用】进行了一层封装,使得我们在编写网络程序的时候,不需要考虑底层API的细节,符合当下面向对象的思想。
IO复用:这次答应我,一举拿下 I/O 多路复用!
【Reactor模式】的 Reactor 的翻译是反应堆,意思是只要来了一个事件,Reactor 就有反应。
Reactor主要包括两个部分:Reactor 和处理资源池。
- Reactor 负责监听事件和分发事件,事件可能是读写事件或连接事件。+ 处理资源池将Reactor 分发给他的事件,主要的流程:Read->处理业务->Send
因此 【Reactor模式】也叫【Dispatcher 模式】,一个主线程负责监听和分发事件,其他的线程处理业务,所以这个名字应该更加的贴切。
目的是减少等待,当遇到需要等待的IO的时候,先释放资源,在IO完成之后,通过事件驱动的方式没继续接下来的处理。
根据 Reactor 的数量和处理资源池线程的数量不同,有 3 种典型的实现:
- 单 Reactor 单线程 + 单 Reactor 多线程 + 主从 Reactor 多线程
3、三种不同的实现
单 Reactor 单线程
话不多说,先看一个图片。
首先从图中可以看到:
- Reactor 处理监听以及事件的分发+ Accept 处理连接+ Handler 处理IO事件以及业务
下面详细介绍下流程:
- 【Reactor】通过 select(IO多路复用接口)监听多个 Socket ,如果 Socket 有事件发生,判断事件类别后看看分配给【Acceptor】,还是【Handler】。+ 如果是建立连接事件,交付给【Acceptor】处理,【Acceptor】会创建一个【Handler】来处理这个刚连接的 Socket 的后续事件。+ 如果是处理非连接事件,交付给【Handler】处理,一般处理过程:read->业务处理->send。
缺点是无法利用CPU多核的能力
应用场景:Redis使用的就是这种模式,但是它是在内存中处理业务的,所以处理起来很快。
单 Reactor 多线程
看一下流程图
流程:
- 【Reactor】通过 select(IO多路复用接口)监听多个 Socket ,如果 Socket 有事件发生,判断事件类别后看看分配给【Acceptor】,还是【Handler】。+ 如果是建立连接事件,交付给【Acceptor】处理,【Acceptor】会创建一个【Handler】来处理这个刚连接的 Socket 的后续事件。+ 如果是处理非连接事件,交付给【Handler】处理。
上面的三个步骤和单 Reactor 单线程方案是一样的,接下来的步骤就开始不一样了:
- 【Handler】 对象不再负责业务处理,只负责数据的接收和发送,【Handler】 对象通过 read 读取到数据后,会将数据发给子线程里的 【Processor】 对象进行业务处理;+ 子线程里的 【Processor】 对象就进行业务处理,处理完后,将结果发给主线程中的 【Handler】对象,接着由 【Handler】通过 send 方法将响应结果发送给 client;
主从 Reactor 多线程
流程:
- 主线程中的 MainReactor 对象通过 select 监控连接建立事件,收到事件后通过 Acceptor 对象中的 accept 获取连接,将新的连接分配给某个子线程;+ 子线程中的 SubReactor 对象将 MainReactor 对象分配的连接加入 select 继续进行监听,并创建一个 Handler 用于处理连接的响应事件。+ 如果有新的事件发生时,SubReactor 对象会调用当前连接对应的 Handler 对象来进行响应。+ Handler 对象通过 read -> 业务处理 -> send 的流程来完成完整的业务流程。