Reactor 和 Proactor模式,IO复用与epoll、同步IO,异步IO与协程

汽车软件中的CPU密集与IO密集任务

在汽车软件中,涉及到ADAS的长期占用CPU的计算任务可以算的上是CPU密集型。

另外的,众多SOA原子服务或者各种数据收集、处理、分发、log系统,应该算是IO密集型任务。

寻求一些手段优化IO性能的原因

在过去开发应用或者中间件时,使用Linux提供的接口,例如直接socket,一般就两种模式,实质上是使用了同步IO:

1、开启循环子线程,阻塞在socket接收处。

2、线程开启循环,周期轮询socket。

这种方式造成了一些弊端,例如对每个socket都需要维护一个子线程,系统给每个线程分配资源造成了资源浪费(如内存)。
虽然阻塞不占用CPU时间,但是如果存在大量socket子线程,线程调度会花费很多CPU时间在进程切换中。造成系统CPU负载率高。

潜在的解决方案

经过研究,一些关键词引起关注。正如本文的标题。

Reactor 和 Proactor模式

Reactor模式和Proactor模式都是基于事件驱动的设计模式,用于处理高并发环境下的I/O多路复用问题。然而,它们在处理I/O事件的方式上有所不同。

Reactor模式是非阻塞同步网络模式,它感知的是就绪可读写事件。具体来说,当某个I/O事件(例如可读就绪)发生时,需要应用进程主动调用相应的read方法来完成数据的读取。这个过程是同步的,即读取完数据后应用进程才能处理数据。因此,Reactor模式可以理解为“来了事件操作系统通知应用进程,让应用进程来处理”。

Proactor模式则是异步网络模式,它感知的是已完成的读写事件。在发起异步读写请求时,需要传入数据缓冲区的地址等信息,这样系统内核才可以自动帮我们把数据的读写工作完成。这里的读写工作全程由操作系统来做,并不需要像Reactor那样还需要应用进程主动发起read/write来读写数据。操作系统完成读写工作后,就会通知应用进程直接处理数据。因此,Proactor模式可以理解为“来了事件操作系统来处理,处理完再通知应用进程”。

总的来说,Reactor和Proactor模式都是基于事件分发的网络编程模式,区别在于Reactor模式是基于“待完成”的I/O事件,而Proactor模式则是基于“已完成”的I/O事件。因此,Proactor模式更加高效,因为它避免了线程间的协作,可以更快地响应I/O事件,但是它的实现相对比较复杂。

IO复用

所谓IO复用,目的是解决前述问题中的每个socket开一个线程的问题。

使用IO复用的机制或者进行IO复用设计(如上文的Reactor模式),使用一个进程监听多个socket。

例如使用一个线程监听多个事件源(如socket),当有事件发生或者可读后,通知对应的程序处理。

落实到Linux内核来帮你做,就变成了Linux IO复用机制,以前有select poll。现在是epoll。

epoll 机制与Reactor

epoll

epoll是Linux下的一个I/O多路复用技术,用于高效地处理大量并发连接。它提供了一组接口,用于注册、修改和删除文件描述符的监听事件。

epoll的接口包括以下几个:

  1. epoll_create():创建一个新的epoll实例,并返回一个指向它的文件描述符。
  2. epoll_ctl():注册、修改或删除文件描述符的监听事件。它需要传入epoll实例的文件描述符、文件描述符、事件类型和回调函数。
  3. epoll_wait():等待注册的文件描述符就绪,并返回就绪的文件描述符列表。它需要传入epoll实例的文件描述符和最大等待时间。
  4. epoll_pwait():与epoll_wait类似,但是可以设置超时时间,并且可以同时处理多个事件。

使用epoll的一般步骤如下:

  1. 创建一个epoll实例,获取其文件描述符。
  2. 使用epoll_ctl()函数注册需要监听的事件和回调函数。
  3. 在需要处理事件的时候,调用epoll_wait()或epoll_pwait()函数等待事件的发生。
  4. 处理就绪的事件。

Reactor

Reactor模式是一种事件驱动的设计模式,用于处理高并发环境下的I/O多路复用问题。它由以下几个组件组成:

  1. Reactor(反应器):是一个接口,定义了注册事件处理器、分发事件和事件处理的方法。通常,Reactor通过异步方式将事件分发给注册的事件处理器。
  2. Event Handler(事件处理器):是一个接口,定义了事件的回调方法。事件处理器实现了Reactor接口,并注册自己感兴趣的事件类型。当事件发生时,Reactor调用事件处理器的回调方法来处理事件。
  3. Concrete Event Handler(具体事件处理器):是事件处理器的实现。在其内部实现了事件处理器的回调方法,进行业务逻辑处理。
  4. Initiation Dispatcher(初始分发器):实际上就是Reactor角色。它本身定义了一些规范,这些规范用于控制事件的调度方式。同时又提供了应用进行事件处理器的注册、删除等操作。它本身是整个事件处理器的核心所在,Initiation Dispatcher会通过Synchronous Event Demultiplexer来等待事件的发生。

工作方式:

  1. 应用程序创建Reactor对象,并注册感兴趣的事件类型和对应的事件处理器。
  2. 当事件发生时,Initiation Dispatcher通过Synchronous Event Demultiplexer将事件传递给对应的事件处理器。
  3. 事件处理器接收到事件后,执行相应的业务逻辑处理。
  4. 处理完成后,事件处理器会再次注册自己感兴趣的事件类型,以便后续继续处理事件。

可以看出,epoll是一个近似于reactor的实现,或者说,基于epoll机制,可以比较方便的实现一个reactor模式来实现事件驱动程序设计。

我们需要做的是,在epoll 的wait返回后,根据返回的socket 就绪列表,去分发,去调用对应的处理程序。

相较于异步IO来说,在分发的时候这个socket还没有被读取,需要应用程序去读取,但是在调用读取的系统调用的时候,也会存在一个阻塞读取数据,例如从内核态拷贝到用户态的过程,我觉得这对目前的工作来讲已经过于玄学了,车端的应用没互联网那么夸张。

Proactor模式 异步IO Boost.asio 协程

Proactor模式是一种消息异步通知的设计模式,它主要用于处理高并发环境下的I/O多路复用问题。以下是Proactor模式的各个组件及其功能和工作方式:

  1. Handle句柄:用于标识socket连接或者是打开文件。在网络服务器中,每个客户连接都会创建不同的套接字句柄,当异步连接、读、写操作执行完成时,完成事件会出现在这些句柄上。
  2. Asynchronous Operation Processor(异步操作处理器):负责执行异步操作,一般由操作系统内核实现。
  3. Asynchronous Operation(异步操作):这是应用程序发出的服务请求,比如异步的通过套接字句柄读写数据。当异步操作激活后,操作不需要借用回调线程的控制即可执行。因此从回调者角度看,操作的执行是异步的。
  4. Completion Event Queue(完成事件队列):异步操作完成的结果会放到队列中,等待后续使用。
  5. Proactor(主动器):为应用程序提供事件循环,从完成事件队列中取出异步操作的结果,分别调用相应的后续处理逻辑。
  6. Completion Handler(完成事件接口):一般是由回调函数组成的接口。
  7. Concrete Completion Handler(完成事件处理逻辑):完成接口定义特定的应用处理逻辑。

在业务流程及时序图中,应用程序启动后,会调用异步处理器提供的异步操作接口来发起异步操作。异步操作处理器接收到操作请求后,会执行相应的异步操作。当操作完成后,会将完成事件放入完成事件队列中。Proactor会不断地轮询完成事件队列,一旦发现有完成事件,就会调用相应的回调函数来处理完成事件。

Boost.Asio是一个广泛使用的C++库,用于处理低级网络编程和并发任务。它实现了Proactor模式,提供了一种高效和灵活的方式处理异步I/O操作。

在Boost.Asio中,Proactor模式的应用主要体现在异步I/O操作的处理上。具体来说,当应用程序发起一个异步操作(如异步读或写)时,Boost.Asio会将其封装为一个异步操作对象,并将其注册到异步事件处理器中。

异步事件处理器使用异步事件分发器(Asynchronous Event Demultiplexer)来等待事件完成。当异步操作完成时,完成事件会被放入完成事件队列中。然后,Proactor会调用异步事件分发器,将完成事件返回给其调用者。

在处理完成事件时,Proactor会调用相应的回调函数。

这个回调函数通常是由应用程序通过boost::bind创建的函数对象,用于处理异步操作的结果。

通过这种方式,Boost.Asio实现了高效的异步I/O处理,使得应用程序可以在不阻塞主线程的情况下处理大量的并发连接和请求。同时,Proactor模式还提供了回调函数的方式,使得应用程序可以灵活地处理完成事件,从而实现了异步编程。

据说,底层是epoll。。

异步IO与协程

之前我想过一个问题,异步IO可以在调用IO操作之后先干别的,不用等待。

在同步(按照时间顺序,典型的C编程)方式下,我调用IO,肯定是要IO的数据,如果不等着拿数据,这个时候能干嘛呢。

直到我看到了协程这个东西,豁然开朗(之前也困惑或协程这个在一个进程内跳来跳去执行的东西有什么用)。

协程和异步IO结合,可以进一步优化IO密集任务。

异步IO,实际上就是给各个事件注册处理函数,让程序在各个处理函数之间跳来跳去,就是纯纯用户态的事情了,甚至可以躲在用户进程里不出来,根本没有切换进程开销,岂不美哉。

你可能感兴趣的:(Linux,C++,linux)