Proactor in Boost.Asio

本文最后更新于:December 1, 2021 am

本文主要对 Asio中的Proactor设计模式相关文档进行了理解和翻译。

​ 主要参考The Proactor Design Pattern: Concurrency Without Threads - 1.72.0 (boost.org)

笔者觉得很有启发性的原文都已经采用引用罗列。

鉴于本人的外语水平和有限的工程经验,博文多有纰漏,恳请大佬在评论区斧正。

总论

Boost的Asio对同步操作和异步操作同时给予支持,其中的异步操作就是基于Proactor的设计模式。

无关平台版的实现

下面来看看Asio如何通过Proactor的方式实现Asio这里的描述是与平台无关的

proactor design pattern

异步操作 Asynchronous Operation

定义了一种异步执行的操作,比如对一个socket的异步读/写

异步操作处理器 Asynchronous Operation Processor

执行异步操作,并且在事件完成时,将此事件Enqueue完成事件队列。 从一个较高层次的角度来看,像reactive_socket_service这样的内部服务都是异步操作处理器。

完成事件队列 Completion Event Queue

缓冲已完成的事件,直到他们被一个异步事件分用器Dequeue

事件完成回调 Completion Handler

用来处理异步操作的结果,是一个函数对象,通常由boost::bind函数的返回值给出。

异步事件分用器 Asynchronous Event Demultiplexer

阻塞地等待事件,直到它出现在完成事件队列中。会向它的调用者返回一个完成事件。

前置器 Proactor

Calls the asynchronous event demultiplexer to dequeue events, and dispatches the completion handler (i.e. invokes the function object) associated with the event. This abstraction is represented by the io_context class.

调用异步事件分用器,将事件出队列dequeue,并且将这个事件分发给事件完成回调(比如,调用函数对象)。这一层抽象由io_context类呈现。

发起器 Initiator

Application-specific code that starts asynchronous operations. The initiator interacts with an asynchronous operation processor via a high-level interface such as basic_stream_socket, which in turn delegates to a service like reactive_socket_service.

应用层面的代码,用于启动异步操作。发起器通过一种高层次的接口(比如basic_stream_socket),同异步操作处理器交互,异步操作处理器轮流将任务委派给服务service(比如reactive_soocket_service)。

用Reactor的实现

在许多平台上,Boost.Asio 通过Reactor来实现Proactor,比如selectepoll或者kqueue。在这种实现下,前置器模式如下:

异步操作处理器 Asynchronous Operation Processor

一个用selectepoll或者kqueue实现的Reactor。当Reactor表示资源可以进行操作时(笔者注:可以理解为内核准备好数据,可以将数据拷贝到用户空间),异步操作处理器执行异步操作,并且将相应的事件完成回调

加入Enqueue 完成事件队列

完成事件队列 Completion Event Queue

一个链表,由事件完成回调(比如一堆函数对象)组成。

异步事件分用器 Asynchronous Event Demultiplexer

等待一个事件或者条件变量,直到在完成事件队列之中有一个事件完成回调可用。

用Windows Overlapped IO实现

Boost.Asio通过利用Windows Overlapped I/O的优势来提高前置器的效率。在这种实现下,前置器模式如下:

异步操作处理器 Asynchronous Operation Processor

由操作系统实现。操作由调用overlapped I/O函数发起,比如AcceptEx

完成事件队列 Completion Event Queue

同样由操作系统实现,同一个I/O Completion Port关联。 每一个io_context实例,只有一个I/O Completion Port

异步事件分用器 Asynchronous Event Demultiplexer

Boost.Asio调用,用于将完成事件和相应的回调对象出队列dequeue

优势

可移植性

Many operating systems offer a native asynchronous I/O API (such as overlapped I/O on Windows) as the preferred option for developing high performance network applications. The library may be implemented in terms of native asynchronous I/O. However, if native support is not available, the library may also be implemented using synchronous event demultiplexors that typify the Reactor pattern, such as POSIX select().

许多操作系统提供一种原生的异步I/O API(比如上文提到的Windows Overlapped I/O),作为推荐的开发高性能网络应用的选项。 库可能由原生的异步I/O API实现。 但是,如果没有原生的异步I/O API支持,库会通过使用同步事件分用器来实现(一个典型的方案是Reactor)。

将线程从并发解耦合

Long-duration operations are performed asynchronously by the implementation on behalf of the application. Consequently applications do not need to spawn many threads in order to increase concurrency.

应用的持续时间较长的操作,被库的异步操作代为执行。结果是,应用不需要通过开大量线程的方式来提高并发度。

性能和可拓展性

Implementation strategies such as thread-per-connection (which a synchronous-only approach would require) can degrade system performance, due to increased context switching, synchronisation and data movement among CPUs. With asynchronous operations it is possible to avoid the cost of context switching by minimising the number of operating system threads — typically a limited resource — and only activating the logical threads of control that have events to process.

以一连接一线程的同步方式为例,因为更多的上下文切换和CPU核心之间的同步和数据搬动,系统性能会下降。有了异步操作,就有可能避免线程上下文切换的代价,方式是通过最小化操作系统线程的数量——通常情况下只是一些很有限的资源,以及额外的,仅仅由于有事件需要处理,从而被激活的逻辑线程。

简化程序和同步操作

Asynchronous operation completion handlers can be written as though they exist in a single-threaded environment, and so application logic can be developed with little or no concern for synchronisation issues.

编写异步操作完成回调时,可以当做在单线程环境下编程,因此在编写应用逻辑时几乎不用担心同步问题。

组合函数

Function composition refers to the implementation of functions to provide a higher-level operation, such as sending a message in a particular format. Each function is implemented in terms of multiple calls to lower-level read or write operations.

函数组合,指的是提供高层次操作的函数的实现(比如使用一种固定的格式发送一条消息),是由多次对低层次的读read或者写write操作的调用组成的。

For example, consider a protocol where each message consists of a fixed-length header followed by a variable length body, where the length of the body is specified in the header. A hypothetical read_message operation could be implemented using two lower-level reads, the first to receive the header and, once the length is known, the second to receive the body.

考虑这样一个场景,有一个协议,协议规定每一套消息都由一个固定长度的消息头Header,再紧接着一个可变长度的消息体Body组成(可变体的长度在Header中被声明)。一个read_message的操作就可以通过两次调用低层次的读read操作实现。第一次读read用于接受消息头Header,然后解析出消息体Body的长度,一旦消息体Body的长度知道了,第二次再调用读read就可以接受消息体Body

To compose functions in an asynchronous model, asynchronous operations can be chained together. That is, a completion handler for one operation can initiate the next. Starting the first call in the chain can be encapsulated so that the caller need not be aware that the higher-level operation is implemented as a chain of asynchronous operations.

为了以异步的模式来组合函数,可以将一堆异步操作链起来。这意味着,一个操作的完成回调可以发起下一个。 而“启动链上的第一次调用”这一行为,可以被封装好,因此调用者不需要知道高层次的操作是被通过底层的链式异步操作实现的。

The ability to compose new operations in this way simplifies the development of higher levels of abstraction above a networking library, such as functions to support a specific protocol.

这种能够通过组合构造新函数的方式简化了在网络库上的高层抽象的开发,比如用于支持某种特定协议的函数。

劣势

编程复杂度

It is more difficult to develop applications using asynchronous mechanisms due to the separation in time and space between operation initiation and completion. Applications may also be harder to debug due to the inverted flow of control.

由于异步操作的发起和完成在时空上的分隔,通常通过异步机制来开发应用会显得更难一些。同时因为控制流被反转,debug可能也更难。

内存使用

Buffer space must be committed for the duration of a read or write operation, which may continue indefinitely, and a separate buffer is required for each concurrent operation. The Reactor pattern, on the other hand, does not require buffer space until a socket is ready for reading or writing.

对于一个读操作或者写操作,必须要有一个缓冲空间,这可能无限地持续下去。并且,对于每一个并发的操作,都需要一个单独的缓冲区。

反观Reactor模式,直到某个socket可读或者可写时,我们才需要一个缓冲区。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!