The Reactor and Proactor design patterns
When using event handling applications, we can follow two approaches to designing the concurrent solution: the Reactor and Proactor design patterns.
These patterns describe the mechanisms followed to process events, indicating how these are initiated, received, demultiplexed, and dispatched. As the system collects and queues the I/O events coming from different resources, demultiplexing these events means separating them to be dispatched to their correct handlers.
The Reactor pattern demultiplexes and dispatches synchronously and serially service requests. It usually follows a non-blocking synchronous I/O strategy, returning the result if the operation can be executed, or an error if the system has no resources to complete the operation.
On the other hand, the Proactor pattern allows demultiplexing and dispatching service requests in an efficient asynchronous way by immediately returning the control to the caller, indicating that the operation has been initiated. Then, the called system will notify the caller when the operation is complete.
Thus, the Proactor pattern distributes responsibilities among two tasks: the long-duration operations that are executed asynchronously and the completion handlers that process the results and usually invoke other asynchronous operations.
Boost.Asio implements the Proactor design pattern by using the following elements:
- Initiator: An I/O object that initiates the asynchronous operation.
- Asynchronous operation: A task to run asynchronously by the OS.
- Asynchronous operation processor: This executes the asynchronous operation and queues results in the completion event queue.
- Completion event queue: An event queue where the asynchronous operation processor pushes events, and the asynchronous event dequeues them.
- Asynchronous event demultiplexer: This blocks the I/O context, waiting for events, and returning completed events to the caller.
- Completion handler: A callable object that will process the results of the asynchronous operation.
- Proactor: This calls the asynchronous event demultiplexer to dequeue events and dispatch them to the completion handler. This is what the I/O execution context does.
Figure 9.3 clearly shows the relationship between all these elements:
Figure 9.3 – Proactor design pattern
The Proactor pattern increases the separation of concerns at the same time as encapsulating concurrency mechanisms, simplifying application synchronization, and increasing performance.
On the other hand, we have no control over how or when the asynchronous operations are scheduled or how efficiently the OS will perform these operations. Also, there is an increase in memory usage due to the completion event queue and increased complexity in debugging and testing.
Another aspect of the design of Boost.Asio is the thread safety of the execution context objects. Let’s now dig into how threading works with Boost.Asio.