6.3 Pool Allocation Pattern
The Static Allocation Pattern only works well for systems that are, well, static in nature. Sometimes you need sets of objects for different purposes at different times during execution. When this is the case, the Pool Allocation Pattern works well by creating pools of objects, created at startup, available to clients upon request. This pattern doesn't address needs for dynamic memory but still provides for the creation of more complex programs than the Static Allocation Pattern.
6.3.1 Abstract
In many applications, objects may be required by a large number of clients. For example, many clients may need to create data objects or message objects as the system operates in a complex, changing environment. The need for these objects may come and go, and it may not be possible to predict an optimal dispersement of the objects even if it is possible to bound the total number of objects needed. In this case, it makes sense to have pools of these objectscreated but not necessarily initialized and available upon request. Clients can request them as necessary and release them back to the pool when they're done with them.
6.3.2 Problem
The prototypical candidate system for the Pooled Allocation Pattern is a system that cannot deal with the issues of dynamic memory allocation, but it is too complex to permit a static allocation of all objects. Typically, a number of similar, typically small, objects, such as events, messages, or data objects, may need to be created and destroyed but are not needed a priori by any particular clients for the entire run-time of the system.
6.3.3 Pattern Structure
Figure 6-3 shows the Pooled Allocation Pattern. The parameterized class Generic Pool Manager is instantiated to create the specific Pooled-
Figure 6-3: Pooled Allocation Pattern
Class required. The instantiated class is shown as Resource Pool Manager. Typically, there will be a number of such instantiated pools in the system, although only one for any specific PooledClass type. Each pool creates and then manages its set of objects, allocating them (and returning a pointer, reference, or handle to the allocated object to the client) and releasing them (putting the released object back into the freeObject list) upon request. Usually, the entire set of pools is created at system startup and never deleted. This removes the problems associated with dynamic memory allocation but preserves many of the benefits.
6.3.4 Collaboration Roles
Client
The Client is any object in the system that has a need to use one or more objects of class resourceClass. To request an object, they call ResourcePool::allocate() and give the object back to the pool by calling ResourcePool::release(). As we will see later in the Implementation Strategies section, in C++, the new() and delete() operators may be overridden to call the allocate() and release() operations to hide the infrastructure from the client.
Generic Pool Manager
Generic Pool Manager is a parameterized (template) class that uses the formal parameters pooledClass and BufferSize to specify the class of the objects pooled and the number of them to create, respectively. The operations of Generic Pool Manager are written to work in terms of these formal parameters.
PooledClass
PooledClass is a formal parameter to the Generic Pool Manager parameterized class. Practically, it may be realized with just about any object desired, but most often, they are simple, small classes used by a variety of clients.
Resource Pool Manager
The Resource Pool Manager class is the instantiated Generic Pool Manager, in which a specific class (ResourceClass) and a specific number of objects (size) are passed as actual parameters. There may be any number of such instantiations in the system but only one per Resource Class.
6.3.5 Consequences
The Pooled Allocation Pattern has a number of advantages over the Static Allocation Pattern. Since memory is allocated at startup and never deleted, there is no problem with the nondeterministic timing of dynamic memory allocation during run-time or memory fragmentation. The system, however, can handle nondeterministic allocation of certain classes of objects. Thus, the pattern scales to more complex systems than does the Static Allocation Pattern. It is especially applicable to systems where a number of common objects may be needed by many different clients, but it cannot be determined at design time how to distribute these objects. This pattern allows the objects to be distributed on an as-needed basis during the execution of the system. Because all pooled objects are created at system startup, the decision about the optimal number of different kinds of objects must be made at design time. For example, it might be decided that as a worst case, 1000 message objects, 5000 data sample objects, and so forth, may be required. If this turns out to be an erroneous decision, the system may fail at startup or later during execution. Further, the system cannot grow to meet new system demands, so the pattern is best applied to systems that are well understood and relatively predictable in their demands on system resources.
A note for Java programmers: This pattern is particularly helpful for Java applications because memory is never released. This pattern avoids fragmentation and the run-time overhead for object allocation, but the garbage collector will still take up some time even though it won't find objects to delete.
6.3.6 Implementation Strategies
This common pattern is fairly easy to implement. To make the pattern easier to use in C++, it is common to rewrite new and delete operators to use the pool manager for the various pooledClass types. That way, the issue of dynamic versus pooled allocation can be hidden from the application programmer.
Code Segment 6-1: C++ Pooled Allocation Implementation Strategy
#include <list> using namespace std; class PoolEmpty { }; // exception type to be thrown // note: list is a container from the C++ STL template <class Resource, int nElements> class GenericPool { list<Resource* > freeList; public: GenericPool(void) { for (int j=0; j<nElements; j++) { freeList.push_back(new Resource); }; }; Resource* allocate(void) { Resource* R; if (freeList.size() > 0) { R = freeList.begin(); // get the first one. freeList.pop_front(); // remove it from the free list return R // and pass it back to the client } else { throw new PoolEmpty; }; }; void release(Resource* R) { freeList.push_back(R); }; }; class BusMessage { string s; }; int main(void) { GenericPool<BusMessage, 1000> busMessagePool; return 0; }
Additionally, this pattern can be mixed with the Factory Pattern of[1] to create the correct subtypes, if desired.
Java has no parameterized types, but it does have collections (arrays) plus some methods for manipulating arrays (in java.util) that are modeled after the Standard Template Library of C++. There are many implementation solutions available. A very simple one was used in Code Segment 6-2. In this example, the LinkedList class from java.util was used to hold the created BusMessage objects. When a client allocates it, the object is removed from the list and passed back to the client. When the client wishes to return the object to the pool, it merely calls BusMessagePool.release(), and the object is reinserted into the pool.
Code Segment 6-2: Java Implementation Strategy for Pools
import java.util.*; class BusMessage { private String s; }; class PoolEmpty extends Exception { }; public class BusMessagePool{ private LinkedList freeList = new LinkedList(); public BusMessagePool() { for (int j=0; j<1000; j++) freeList.addLast(new BusMessage()); }; // // allocate() gives the client a reference to a // BusMessage object and removes it from the free list public BusMessage allocate() throws PoolEmpty { BusMessage B; if (freeList.size() > 0) { B = (BusMessage) freeList.getFirst(); // get the first one. freeList.removeFirst(); // remove it from the // free list and pass return B; // it back to the // client } else { throw new PoolEmpty(); }; }; // release() returns the passed BusMessage object back into the pool public void release(BusMessage Carcass) { freeList.addFirst(Carcass); }; }
Whatever the underlying basis for the pools, there must be a separate Resource Pool Manager for each kind of pooledClass. In C++, this is simply a matter of binding a different class to the formal parameter list of the ResourcePool template. In Java, this can be done by creating different lists using the LinkedList containers.
6.3.7 Related Patterns
The Pooled Allocation Pattern is but one of many approaches to managing memory allocation. The Static Allocation Pattern can be used for systems that are simpler, and the Dynamic Allocation Pattern and its variants can be used for more complex needs. The Abstract Factory Pattern [1] can be used with this pattern to provide a means for Pooled Allocation for different environments.
6.3.8 Sample Model
Figure 6-4a shows the object model for a system running a class model derived from the pattern shown in Figure 6-3. Figure 6-4b shows a scenario of the objects as they run. The first message shows the creation of the TempDataPool object, which in turn creates the 1000 TempData objects that it will manage. The other part of the object model shows three clients of the TempDataPool.
TempSensor This is a thermometer that records the temperature every ½ second and in doing so, allocates a TempData object to store the information. This object reports the temperature (by passing a reference to the allocated TempData object), first to the TempView, a GUI view object, and then to TempHistory, which manages the history of Temperature over the last several seconds.
TempView This is a GUI object that displays the temperature to a user on a display. Once it has displayed the value, it releases the TempData object that was passed back to the pool
TempHistory This maintains a history of the last ten seconds of temperature data. Thus, for the first 20 samples, it does not delete the TempData objects passed to it, but subsequently, it releases the oldest TempData object it owns when it receives a new one.
Figure 6-4: Pooled Allocation Pattern Example