常见多路IO复用并发模型提纲

模型1-单线程Accept

create ListenFd; //创建一个socket
Bind+Listen; //绑定到ip+端口,并且将其设置为监听socket
Accept(Listenfd); //从这个监听socket中返回一个连接描述符(这个是阻塞的,直到有连接请求过来)
① 主线程main thread执行阻塞Accept,每次客户端Connect链接过来,main thread中accept响应并建立连接
② 创建链接成功,得到Connfd1套接字后, 依然在main thread串行处理套接字读写,并处理业务。
③ 在②处理业务中,如果有新客户端Connect过来,Server无响应,直到当前套接字全部业务处理完毕。
④ 当前客户端处理完后,完毕链接,处理下一个客户端请求。
单线程Accept的特点就是,我们代码执行完Accept请求后,往往就会去一个while(1)的循环中去进行和这个连接的通信,代码并没有在accept这里执行,所以,如果你这个时候另一个Client也发来一个连接请求,不会立即处理它,只会等前面那个连接通信完后,代码转去执行Accept,才会接收到这个连接,然后处理...
优点:
- socket编程流程清晰且简单,适合学习使用,了解socket基本编程流程。
缺点:
- 该模型并非并发模型,是串行的服务器,同一时刻,监听并响应最大的网络请求量为
1。 即并发量为1。 - 仅适合学习基本socket编程,不适合任何服务器Server构建。
模型2-单线程Accept+多线程读写业务

① 主线程main thread执行阻塞Accept,每次客户端Connect链接过来,main thread中accept响应并建立连接
② 创建链接成功,得到Connfd1套接字后,创建一个新线程thread1用来处理客户端的读写业务。main thead依然回到Accept阻塞等待新客户端。
③ thread1通过套接字Connfd1与客户端进行通信读写。
④ server在②处理业务中,如果有新客户端Connect过来,main thread中Accept依然响应并建立连接,重复②过程。
优点:
- 基于
模型一:单线程Accept(无IO复用)支持了并发的特性。 - 使用灵活,一个客户端对应一个线程单独处理,
server处理业务内聚程度高,客户端无论如何写,服务端均会有一个线程做资源响应。
缺点:
- 随着客户端的数量增多,需要开辟的线程也增加,客户端与server线程数量
1:1正比关系,一次对于高并发场景,线程数量受到硬件上限瓶颈。线程过多也会也会增加CPU的线程切换成本,降低CPU的利用率 - 对于长链接,客户端一旦无业务读写,只要不关闭,server的对应线程依然需要保持连接(心跳、健康监测等机制),占用连接资源和线程开销资源浪费。
- 仅适合客户端数量不大,并且数量可控的场景使用。
- 仅适合学习基本socket编程,不适合任何服务器Server构建。
模型3-单线程多路IO复用

最开始,我们的监听IO集合中就只有一个ListenFd,随着连接的增多,也会有很多的连接加入到这些监听IO集合
① 主线程main thread创建listenFd之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求,I/O复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1加入到监听I/O集合中。
② Client1再次进行正常读写业务请求,main thread的多路I/O复用机制阻塞返回,会触该套接字的读/写事件等。
③ 对于Client1的读写业务,Server依然在main thread执行流程提继续执行,此时如果有新的客户端Connect链接请求过来,Server将没有即时响应。
④ 等到Server处理完一个连接的Read+Write操作,继续回到多路I/O复用机制阻塞,其他链接过来重复 ②、③流程。
优点:
- 单流程解决了可以同时监听多个客户端读写状态的模型,不需要
1:1与客户端的线程数量关系。 - 多路I/O复用阻塞,非忙询状态,不浪费CPU资源, CPU利用率较高。
缺点:
- 虽然可以监听多个客户端的读写状态,但是同一时间内,只能处理一个客户端的读写操作,实际上读写的业务并发为1。
- 多客户端访问Server,业务为串行执行,大量请求会有排队延迟现象,如图中⑤所示,当
Client3占据main thread流程时,Client1,Client2流程卡在IO复用等待下次监听触发事件。
如果并发量较小,企业开发中这是可以采用的。比如说已经固定了的并发量,比如一个管理端去管几个子节点。
模型4-单线程多路IO复用+多线程读写业务(业务工作池)

① 主线程main thread创建listenFd之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求,I/O复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1加入到监听I/O集合中。
② 当connFd1有可读消息,触发读事件,并且进行读写消息
③ main thread按照固定的协议读取消息,并且交给worker pool工作线程池, 工作线程池在server启动之前就已经开启固定数量的thread,里面的线程只处理消息业务,不进行套接字读写操作。
④ 工作池处理完业务,触发connFd1写事件,将回执客户端的消息通过main thead写给对方。
也就是说,以前是我们接收到客户端发送过来数据时,我们需要进行读到这个数据,然后经过一定的处理(比如查询数据库)后,发送数据给这个客户端。
在这里,我们读了消息之后,交给线程池去处理这个事件的业务,它处理完后会去触发写事件,然后下次多路IO复用就会检测到这个写事件,然后就去写(也就是一个异步的事件处理程序)
优点:
- 与
模型三相比,这里将业务处理部分,通过工作池分离出来,减少多客户端访问Server,业务为串行执行,大量请求会有排队延迟时间。 - 实际上读写的业务并发为1,但是业务流程并发为worker pool线程数量,加快了业务处理并行效率。
缺点:
- 读写依然为
main thread单独处理,最高读写并行通道依然为1. - 虽然多个worker线程处理业务,但是最后返回给客户端,依旧需要排队,因为出口还是
main thread的Read + Write
模型5-单线程IO复用+多线程IO复用(链接线程池)
① Server在启动监听之前,开辟固定数量(N)的线程,用Thead Pool线程池管理
② 主线程main thread创建listenFd之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求,I/O复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1分发给Thread Pool中的某个线程进行监听。
③ Thread Pool中的每个thread都启动多路I/O复用机制(select、epoll),用来监听main thread建立成功并且分发下来的socket套接字。
④ 如图, thread监听ConnFd1、ConnFd2, thread2监听ConnFd3,thread3监听ConnFd4. 当对应的ConnFd有读写事件,对应的线程处理该套接字的读写及业务。
在这里,我们先创建固定数量的线程,我们的主线程只用于监听
ListenFd,连接IO交给子线程去处理(放进子线程的多路IO复用)。客户端与我们连接的时候,我们会Accept(ListenFd),他不是把这个连接交给主线程的IO复用,而是把这个连接分发给某一个子线程。(这个分发方式,可以采用依次放各个线程的方式,或者一些基本负载的方式)
优点:
- 将
main thread的单流程读写(模型三),分散到多线程完成,这样增加了同一时刻的读写并行通道,并行通道数量N,N为线程池Thread数量。 - server同时监听的
ConnFd套接字数量几乎成倍增大,之前的全部监控数量取决于main thread的多路I/O复用机制的最大限制***(select 默认为1024, epoll默认与内存大小相关,约3~6w不等)***,所以理论单点Server最高响应并发数量为N*(3~6W)(N为线程池Thread数量,建议与CPU核心成比例1:1)。 - 如果良好的线程池数量和CPU核心数适配,那么可以尝试CPU核心与Thread进行绑定,从而降低CPU的切换频率,提升每个
Thread处理合理业务的效率,降低CPU切换成本开销。
缺点:
- 虽然监听的并发数量提升,但是最高读写并行通道依然为
N,而且多个身处同一个Thread的客户端,会出现读写延迟现象,实际上每个Thread的模型特征与模型三:单线程多路IO复用一致。
这个模型是目前应用最广泛的模型!!!是企业开发最常用的
模型5-进程版
与五、单线程IO复用+多线程IO复用(链接线程池)无大差异。
不同处
- 进程和线程的内存布局不同导致,
main process(主进程)不再进行Accept操作,而是将Accept过程分散到各个子进程(process)中. - 进程的特性,资源独立,所以
main process如果Accept成功的fd,其他进程无法共享资源,所以需要各子进程自行Accept创建链接 main process只是监听ListenFd状态,一旦触发读事件(有新连接请求). 通过一些IPC(进程间通信:如信号、共享内存、管道)等, 让各自子进程Process竞争Accept完成链接建立,并各自监听。
优缺点:
与五、单线程IO复用+多线程IO复用(链接线程池)无大差异。
不同处:
-
多进程内存资源空间占用稍微大一些
-
多进程模型安全稳定型较强,这也是因为各自进程互不干扰的特点导致。
模型6-单线程多路I/O复用+多线程多路I/O复用+多线程
① Server在启动监听之前,开辟固定数量(N)的线程,用Thead Pool线程池管理
② 主线程main thread创建listenFd之后,采用多路I/O复用机制(如:select、epoll)进行IO状态阻塞监控。有Client1客户端Connect请求,I/O复用机制检测到ListenFd触发读事件,则进行Accept建立连接,并将新生成的connFd1分发给Thread Pool中的某个线程进行监听。
③ Thread Pool中的每个thread都启动多路I/O复用机制(select、epoll),用来监听main thread建立成功并且分发下来的socket套接字。一旦其中某个被监听的客户端套接字触发I/O读写事件,那么,会立刻开辟一个新线程来处理I/O读写业务。
④ 但某个读写线程完成当前读写业务,如果当前套接字没有被关闭,那么将当前客户端套接字如:ConnFd3重新加回线程池的监控线程中,同时自身线程自我销毁。
优点:
- 在
模型五、单线程IO复用+多线程IO复用(链接线程池)基础上,除了能够保证同时响应的最高并发数,又能解决读写并行通道局限的问题。 - 同一时刻的读写并行通道,达到
最大化极限,一个客户端可以对应一个单独执行流程处理读写业务,读写并行通道与客户端数量1:1关系。
缺点:
- 该模型过于理想化,因为要求CPU核心数量足够大。
- 如果硬件CPU数量可数(目前的硬件情况),那么该模型将造成大量的CPU切换成本浪费。因为为了保证读写并行通道与客户端
1:1的关系,那么Server需要开辟的Thread数量就与客户端一致,那么线程池中做多路I/O复用的监听线程池绑定CPU数量将变得毫无意义。 - 如果每个临时的读写
Thread都能够绑定一个单独的CPU,那么此模型将是最优模型。但是目前CPU的数量无法与客户端的数量达到一个量级,目前甚至差的不是几个量级的事。
总结
综上,我们整理了7中Server的服务器处理结构模型,每个模型都有各自的特点和优势,那么对于多少应付高并发和高CPU利用率的模型,目前多数采用的是模型五(或模型五进程版,如Nginx就是类似模型五进程版的改版)。
至于并发模型并非设计的约复杂越好,也不是线程开辟的越多越好,我们要考虑硬件的利用与和切换成本的开销。模型六设计就极为复杂,线程较多,但以当今的硬件能力无法支撑,反倒导致该模型性能极差。所以对于不同的业务场景也要选择适合的模型构建,并不是一定固定就要使用某个来应用。