During the study of the Boost library, I’ve stumbled on Proactor pattern. This is a design pattern intended to handle I/O operations asynchronously, but let’s describe other alternatives first.
Massive web servers should serve a lot of active connections in a short period of time; so, it’s important to do this most effectively with a less overhead.
Several well-known methods exist:
Let’s describe them in order.
This model is quite straightforward. The server performs all work in a single thread, sequentially accepting a connection, serving it, blocking for every I/O operation, until the connection closes.
Writing such a server is as easy as ABC. However, it obviously is a bad solution, since only one connection can be serviced at a time (i.e. no concurrency). This strategy is not suitable for long-term connections and just slow.
This image shows server activity over time. Grey color represents waiting for I/O; other colors represent actively serving some request.
Synchronous Multi-threading model
Threading model is also well-known. Its essence is to create accept-fork loop which will accept every incoming connection and fork a new thread to serve it. Each thread serves connection in a synchronous manner. Thus, an application can take advantage of multiprocessor machines and handle multiple connections simultaneously.
While this method is much better than synchronous one, it still has drawbacks. The main one is that, as far as waiting for I/O occupies most of the time, application performance will suffer from numerous context switches and forks. The second disadvantage is that threads often require some form of synchronization so that application will be more complicated; as well as additional time will be spent waiting on semaphores and mutexes.
The Proactor pattern represents an asynchronous model.
The main idea is to avoid the overhead of forking and context switching by using asynchronous operations. Asynchronous operations allow running I/O actions in the background. In this way, the application is able to perform multiple I/O operations simultaneously and may remain single-threaded at the same time.
The application initiates asynchronous operation with the OS and passes Completion Handler and a reference to the Completion Dispatcher that will be used to notify the application upon completion of the asynchronous operation;
Then the application invokes the event loop of the Completion Dispatcher;
When any transaction completes, Completion Dispatcher will be notified and will call specified Completion Handler;
it, in turn, may initiate other asynchronous operations;
You can find examples in Boost.Asio documentation.
Examples are more complicated than possible synchronous multi-threaded solutions. But if you try, you can quickly figure out what’s going on.
Proactor pattern eliminates fork and context-switch overhead;
Furthermore, as far as application is single-threaded, there is little or no need for thread synchronization;
However, it doesn’t limit the number of threads; the Completion Dispatcher encapsulates the concurrency mechanism so various concurrency strategies may be implemented including single-threaded and Thread Pool solutions.
Like any other design pattern, Proactor has its drawbacks:
Program complexity increases;
Instead of a linear programming model, you should separate connection handling in a of functions which will set each other as a completion handler for some operation.
Asynchronous events are hard to debug;
Because all operations are asynchronous, it’s hard to track an order of program execution.