七叶笔记 » golang编程 » linux多线程编程之Reactor模型到muduo多线程模型

linux多线程编程之Reactor模型到muduo多线程模型

推荐视频:

c/c++ linux服务器开发学习地址:

Reactor

Reactor 模式,市面上常见的开源软件很多都采用了这个方案,比如 Redis、Nginx、Netty

演进

1、一个线程处理一个连接,处理完毕之后释放线程

2、资源复用,创建线程池,一个线程处理多个连接,线程会阻塞在read操作上,解决方式:

  1. 将socket改为非阻塞,不断轮询,但是消耗CPU资源
  2. I/O多路复用,当连接上有数据时,才发起请求。

对 I/O 多路复用作了一层封装,让使用者不用考虑底层网络 API 的细节,只需要关注应用代码的编写。这种模式就是Reactor(对事件反应)模式。该模式又叫Dispatcher模式。

Reactor 模式主要由 Reactor 和处理资源池这两个核心部分组成,它俩负责的事情如下:

  • Reactor 负责监听和分发事件,事件类型包含连接事件、读写事件;
  • 处理资源池负责处理事件,如 read -> 业务逻辑 -> send;

Reactor和处理资源池都可以有单个或者多个,有三种模式比较经典:

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

Reactor

单 Reactor 单进程 / 线程

Reactor对象:监听和分发事件

Acceptor对象:获取连接

Handler对象:处理业务

缺点

单 Reactor 多线程 / 多进程

单Reactor多线程:

Handler通过read读取到数据后,会将数据发送个子线程里的Processor对象进行业务处理。子线程里的Processor对象进行业务处理,处理完毕之后将结果发送给主线程的Handler对象,由Handler调用send方法将响应结果发送给client

优势:

能够充分利用多核CPU

缺陷:

共享资源的竞争. 单Reactor模式还有一个缺点,就是一个Reactor对象承担所有事件的监听和响应,在面对瞬间高并发的场景时,容易成为性能瓶颈.

单Reactor多进程:

实现起来比单Reactor多线程麻烦,父子之间的双向通行。多线程的共享数据比多进程间的通信的复杂度低。实际中也见不到单Reactor多进程的模式。

【文章福利】需要C/C++ Linux服务器架构师学习资料加群812855908(资料包括C/C++,Linux,golang技术,内核,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等)

多 Reactor 多进程 / 线程

方案详细介绍:

  • 主线程的MainReactor对象通过I/O多路复用监控建立连接事件,收到事件后通过Acceptor建立连接。将连接分配给子线程。
  • 子线程的SubReactor收到主线程发来的事件后,将该事件加入到子线程的I/O多路复用中继续监控,并创建一个Handler来进行响应。
  • 当已连接的套接字有事件发生时,会调用对应的Handler来处理:read——>业务处理——>send

实现比单Reactor多进程/线程简单:

  • 主线程和子线程分工明确,主线程只负责接收新连接,子线程负责后续的业务处理。
  • 主线程和子线程之间的交互简单,只需要主线程把新来的连接传给子线程。

Proactor

Reactor是非阻塞的同步网络模式,而Proactor是异步网络模式

先来看看阻塞和非阻塞:

阻塞

当用户调用read函数,线程会被阻塞,一直到数据准备好,并把数据从内核缓冲区拷贝到应用程序的缓冲区,当拷贝完成,read才会返回。

非阻塞

非阻塞的read请求在数据未准备好的时候立即返回,可以继续往下执行,应用程序应该不断地轮询内核,直到数据准备好,将数据从内核缓冲区拷贝到应用程序缓冲区,read调用才可以获取到结果。

注意最后一次read调用,获取数据的过程是一个同步的过程,是需要等待的过程。这里的同步是指的是内核态的数据拷贝到用户程序缓冲区的这个过程。

再来看异步I/O:

异步I/O

【内核数据准备好】和【数据从内核态拷贝到用户态】都不需要等待。

发起 aio_read (异步 I/O) 之后,就立即返回,内核自动将数据从内核空间拷贝到用户空间,这个拷贝过程同样是异步的,内核自动完成的,和前面的同步操作不一样,应用程序并不需要主动发起拷贝动作。

Proactor 正是采用了异步 I/O 技术,所以被称为异步网络模型。

Reactor和Proactor的区别

Reactor是非阻塞同步网络模式,感知的是就绪可读写事件

Proactor是异步网络模式,感知的是已完成的读写事件

muduo中的Reactor

muduo的Reactor,采用的是多Reactor多线程的模型,但是有一些差异

Reactor时序图:

muduo 的整体风格受到 Netty 的影响,整个架构依照 Reactor 模式,基本与如下图所示相符:

muduo的线程模型:

muduo 默认是单线程模型的,即只有一个线程,里面对应一个 EventLoop。这样整体对于线程安全的考虑可能就比较简单了,

但是 muduo 也可以支持主从reactor模式:

主从Reactor模式

主从 reactor 是 Netty 的默认模型,一个 reactor 对应一个 EventLoop。主 Reactor 只有一个,只负责监听新的连接,accept 后将这个连接分配到子 Reactor 上。子 Reactor 可以有多个。这样可以分摊一个 Eventloop 的压力,性能方面可能会更好。如下图所示:

其中主 Reactor 的 EventLoop 就是 TcpServer 的构造函数中的 EventLoop* 参数。Acceptor 会在此 EventLoop 中运行。

而子 Reactor 可以通过 TcpServer::setThreadNum(int) 来设置其个数。因为一个 Eventloop 只能在一个线程中运行,所以线程的个数就是子 Reactor 的个数。

ioLoop和loop_间的线程切换都发生在连接建立和断开的时刻,不影响正常业务的性能。断开连接时,会从子loop切换到主loop。

如果设置了子 Reactor,新的连接会通过 Round Robin 的方式分配给其中一个 EventLoop 来管理。如果没有设置子 Reactor,则是默认的单线程模型,新的连接会再由主 Reactor 进行管理。

但其实这里似乎有些不合适的地方:每个 TcpServer 有自己的EventLoopThreadPool,多个TcpServer之间不共享EventLoopThreadPool。子 Eventloop 线程池不能共享,这个是每个 TcpServer 独有的。不过 Netty 的主 EventLoop 和子 Eventloop 池都是可以共享的。

相关文章