Beautiful Code [297]
Our use of wrapper facades allows us to lock/unlock mutexes, listen on a particular IPC mechanism to accept new connections, and wait for multiple I/O events concisely, efficiently, and portably. Without these useful abstractions, we would have had to write many lines of tedious and error-prone code that would be hard to understand, debug, and evolve.
The benefits from these abstractions become more apparent with more complex concurrent logging servers shown next, as well as with more complex framework use cases, such as graphical user interfaces []or communication middleware. [§§§]
[] Gamma et al., op. cit.
[§§§]Schmidt et al., op. cit.
Labor-Saving Architecture: An Object-Oriented Framework for Networked Software > Implementing Concurrent Logging Servers
26.4. Implementing Concurrent Logging Servers
To overcome the scalability limitations of the iterative and reactive servers shown in the previous sections, the logging servers in this section use OS concurrency mechanisms: processes and threads. Using the APIs provided by operating systems to spawn threads or processes, however, can be a daunting task due to accidental complexities in their design. These complexities stem from semantic and syntactic differences that exist not only between different operating systems, but also different versions of the same operating system. Our solution to these complexities is again to apply wrapper facades that provide a consistent interface across platforms and integrate these wrapper facades into our OO Logging_Server framework.
26.4.1. A Thread-per-Connection Logging Server
Our thread-per-connection logging server (TPC_Logging_Server) runs a main thread that waits for and accepts new connections from clients. After accepting a new connection, a new worker thread is spawned to handle incoming log records from that connection. Figure 26-10 shows the steps in this process.
Figure 26-10. Steps in the thread-per-connection logging server
The main loop for this particular logging server differs from the steps depicted in Figure 26-3 because the call to handle_data( ) is not necessary, as the worker threads are responsible for that call. There are two ways to handle this situation:
We could note that the base run( ) method calls handle_data( ) with the default argument of a NULL pointer, and simply have our implementation exit immediately for that input.
We could simply override the run( ) method with our own implementation that omits this call.
The second solution may at first appear advantageous because it avoids a virtual method call to handle_data( ). The first solution is better in this case, however, because the performance hit of that virtual call is not a limiting factor, and overriding the run( ) template method would prevent this class from benefiting from changes to the base class implementation, potentially causing it to fail in subtle and pernicious ways.
The main challenge here is implementing the concurrency strategy itself. As with the Iterative_Server in the earlier section "An Iterative Logging Server," the wait_for_ multiple_events( ) method is superfluous because our main loop simply waits for new connections, so it is sufficient for handle_connections( ) to block on accept( ) and subsequently spawn worker threads to handle connected clients. Our TPC_Logging_Server class must therefore provide a method to serve as an entry point for the thread. In C and C++, a class method may serve as an entry point to a thread only if the class is defined as static, so we define the TPC_Logging_Server::svc( ) static class method.
At this point, we have an important design decision to make: what exactly does the thread entry point do? It is tempting to simply have the svc( ) method itself perform all of the work necessary to receive log records from its associated connection. This design is less than ideal, however, because static methods cannot be virtual, as that would cause problems if we later derive a new logging server from this implementation to change the way it handles data events. Application