Proactor 和 Reactor 之间的真正区别是什么?
我认为对于 Reactor 和 Proactor 设计模式之间的具体差异有多种定义。我目前的想法是,最好关注重复阅读的语义。
Reactor:用户向reactor注册通道,提供回调。此后,每当有新数据可供读取时,反应器就会通过调用此回调来通知用户。然后,用户可以在回调中从通道读取一些数据。因此,提供单个回调(在注册时)可能会导致多次调用此回调。
前摄器:每当用户想要从通道读取一些数据时,他们都会向前摄器发出异步请求,提供回调和指向某些缓冲区空间的指针/大小。此后,当通道中可以读取新数据时,前摄器会将此数据复制到用户提供的缓冲区空间中并调用回调。这样就完成了异步请求。用户必须确保缓冲区空间保持有效/在范围内/可写,直到异步请求完成。如果用户想要读取更多数据,则必须显式发出另一个异步请求。因此(在请求时)提供单个回调总是会导致回调的一次调用。在这种模式中,通常会编写回调,以便在回调结束时发出另一个异步请求(这在 ASIO 文档中称为“异步操作链”;ASIO 遵循 Proactor 模式)。
Linux API epoll (=reactor) 和 io_uring (=proactor) 可以体现这种差异。使用 epoll,您可以通过
::epoll_ctl(fd, EPOLL_CTL_ADD, ...)
将文件描述符添加到内核 epoll 对象一次,然后该文件描述符可以在 ::epoll_wait(...)
的结果中多次出现。使用 io_uring,用户每提交一个提交队列条目(sqe)都会收到一个完成队列条目(cqe)(但我对 io_uring 不太熟悉;我在这里可能是错的)。
因此,当存在大量短读取时,前摄器模式会导致更多开销。我怀疑这就是io_uring有时会比epoll慢的原因:
不过,可以给出不同的定义。例如,定义可以围绕是否(a.1)用户提供指向某个缓冲区空间的指针/大小,并保证在异步请求完成之前该缓冲区空间有效,或者(a.2)用户是否主动读取缓冲区空间。数据进入回调内的缓冲区。人们还可以将定义集中在是否使用(b.1)回调(用户可以对事件做出反应)或是否(b.2)用户主动检查某些通道可读的数据结构。不过,这些方面与上述方面是正交的,我发现将定义集中在(c.1)用户回调通常是否必须主动发出新的异步请求(c.2)是最有帮助的.
Proactor 或 Reactor 主要是关于我们如何主动或被动地处理 IO 读/写: