The idea behind object pooling is just as the name implies: COM+ can maintain a pool of objects that are already created and ready to serve clients. The pool is created per object type; different objects types have separate pools. You can configure each component type pool by setting the pool parameters on the component’s properties Activation tab (as shown in Figure 3-3). With object pooling, for each object in the pool, you pay the cost of creating the object only once and reuse it with many clients. The same object instance is recycled repeatedly for as long as the containing application runs. The object’s constructor and destructor are each called only once. Object pooling is an instance management technique designed to deal with the interaction pattern of Internet clients—numerous clients creating objects for every request, not holding references on the objects, but releasing their object references as soon as the request processing is done. Object pooling is useful when instantiating the object is costly or when you need to pool access to scant resources. Object pooling is most appropriate when the object initialization is generic enough to not require client-specific parameters. When using object pooling, you should always strive to perform in the object’s constructor as much as possible of the time-consuming work that is the same for all clients, such as acquiring connections (OLEDB, ADO, ODBC), running initialization scripts, initializing external devices, creating file handles, and fetching initialization data from files or across a network. Avoid using object pooling if constructing a new object is not a time-consuming operation because the use of a pool requires a fixed overhead for pool management every time the client creates or releases an object.
Any COM+ application, whether a server or a library application, can host object pools. In the case of a server application, the scope of the pool is the machine. If you install proxies to that application on other machines, the scope of the pool can be the local network. In contrast, if the application is a library application, then a pool of objects is created for each client process that loads the library application. As a result, two clients in different processes will end up using two distinct pools. If you would like to have just one pool of objects, configure your application to be a server application.
When a client issues a request to create a component instance and that component is configured to use object pooling, instead of creating the object, COM+ first checks to see if an available object is in the pool. If an object is available, COM+ returns that object to client. If there is no available object in the pool and the pool has not yet reached its maximum configured size, COM+ creates a new object and hands it back to the creating client. In any case, once a client gets a reference to the object, COM+ stays out of the way. In every respect except one, the client’s interaction with the object is the same as if it were a nonpooled object. The exception occurs when the client calls the final release on the object (when the reference count goes down to zero). Instead of releasing the object, COM+ returns it to the pool. Figure 3-4 describes this life cycle graphically in a UML activity diagram.[1]
If the client chooses to hold onto the pooled object for a long time, it is allowed to do so. Object pooling is designed to minimize the cost of creating an object, not the cost of using it.
To use object pooling for a given component, you should first enable it by selecting the “Enable object pooling” checkbox on component’s Activation tab. The checkbox allows you to enable or disable object pooling. The two other parameters let you control the pool size and the object creation timeout. The minimum pool size determines how many objects COM+ should keep in the pool, even when no clients want an object. When an application that is configured to contain pools of objects is first launched, COM+ creates a number of objects for each pool equal to the specified minimum pool size for the application. If the minimum pool size is zero, COM+ doesn’t create any objects until the first client request comes in. Minimum pool size is used to mitigate sudden spikes in demand by having a cache of ready-to-use, initialized objects. The minimum pool size must be less than the maximum pool size, and the Component Services Explorer enforces this condition.
The maximum pool size configuration is used to control the total
number of objects that can be created, not just how many objects the
pool can contain. For example, suppose you configure the pool to have
a minimum size of zero and a maximum of four. When the first creation
request comes in, COM+ simply creates an object and hands it over to
the client. If a second request comes in and the first object is
still tied up by the first client, COM+ creates a new object and
hands it over to the second client. The same is true for the third
and fourth clients. However, when a fifth request comes along, four
objects are already created and the pool has reached its maximum
potential size, even though it is empty. Once you reach that limit
and all objects are in use, further clients requests for objects are
blocked until an object is returned to the pool. At that time, COM+
hands it over to the waiting client. If, on the other hand, the
client waited for the duration specified in the timeout field, the
client is unblocked and CoCreateInstance( )
returns the error code
CO_E_ACTIVATIONFAILED_TIMEOUT
(not E_TIMEOUT
,
as documented in the COM+ section of the MSDN). COM+ maintains a
queue for each pool of waiting clients to handle the situation in
which more than one client is blocked while waiting for an object to
become available. COM+ services the clients in the queue on a
first-come, first-served basis as objects are returned to the pool. A
creation timeout of zero causes all client calls to fail, regardless
of the state of the pool and availability of objects.
If the pool contains more objects than the configured minimum size, COM+ periodically cleans the pool and destroys the surplus objects. There is no documentation of when or how COM+ decides to do the cleanup.
Deciding on the minimum and maximum pool size configuration depends largely on the nature of your application and the work performed by your objects. For example, the pool size can be affected by:
Expected system load highs and lows
Performance profiling done on your product to optimize the usage of resources
Various parameters captured during installation, such as user preferences and memory size
The number of licenses your customer has paid for; you can set the pool size to that number and have an easy-to-manage licensing mechanism
In general, when configuring your pool size, try to balance available resources. You usually need to trade memory used to maintain a pool of a certain size and the pool management overhead in exchange for faster client access and use of objects.
When you want to pool instances of
your component, you must adhere to certain requirements and
constraints. COM+ implements object pooling by
aggregating your object in a COM+ supplied
wrapper. The aggregating wrapper’s implementation of
AddRef( )
and Release( )
manage
the reference count and return the object to the pool when the client
has released its reference. Your component must therefore support
aggregation to be able to use object pooling. When you import a COM
component into a COM+ application, COM+ verifies that your component
supports aggregation. If it does not, COM+ disables object pooling in
the Component Services Explorer. If you implement your object using
ATL, make sure your code does not
contain the ATL macro DECLARE_NOT_AGGREGATABLE( )
,
as this macro prevents your object from being aggregated. By default,
the Visual C++ 6.0 ATL Wizard inserts this macro into your
component’s header file when generating MTS components. You
must remove this macro to enable object pooling (it is safe to do
so—there are no side effects in COM+).
Another design point to pay attention to is
your pooled object’s
threading model. A pooled object should
have no thread affinity of any sort—it should make no
assumption about the identity of the thread it executes on, or use
thread local storage, because the execution thread can be different
each time the object is pulled from the pool to serve a client. The
pooled object therefore cannot use
the single-threaded
apartment
model (STA)
because STA objects always require execution on the same thread. When
you import a component to a COM+ application, if the
component’s threading model is marked as apartment (STA), COM+
disables object pooling for that component. A pooled object can only
use the free multithreaded
apartment
model (MTA), the
both model, or the neutral threaded
apartment
model (NTA, covered
in Chapter 5). If performance is important to you,
you may want to base your pooled component’s threading model on
your clients’ threading model. If your clients are
predominantly STA-based, mark your component as
Both
so that it can be loaded directly in the
client’s STA. If your clients are predominantly MTA based, mark
your component as either Free
or
Both
(the Both
model also
allows direct use by STA clients). If your clients are of no
particular apartment designation, mark your component as
Neutral
. For most practical purposes, the
neutral-threading model should be the most flexible and
performance-oriented model. Table 3-1 summarizes
these decisions.
Table 3-1. Pooled object threading model
Clients threading model |
Recommended pooled object threading model |
---|---|
No particular model |
NTA |
STA |
Both |
MTA |
Both/MTA |
Both |
Both |
NTA |
NTA |
Deciding not to use STA has two important consequences:
Pooled objects cannot display a user interface because all user interfaces require the STA message loop.
You cannot develop pooled objects using Visual Basic 6.0 because all COM components developed in Version 6 are STA based and use thread local storage. The next version of Visual Basic, called Visual Basic.NET, allows you to develop multithreaded components.
When
a pooled object is placed in
the pool, it does not have any context. It is in stasis—frozen
and waiting for the next client activation request. When it is
brought out of the pool, COM+ uses its usual context activation logic
to decide in which context to place the object—in its
creator’s context (if the two are compatible) or in its own new
context. From the object’s perspective, it is always placed in
a new context; different from the one it had the last time it was
activated.
Objects often require context-specific
initialization, such as retrieving interface pointers or fine-tuning
security. Object pooling only saves you the cost of reconstructing a
new object and initializing it to generic state. Each time an object
is activated, you must still do a context-specific initialization,
and you benefit from using object pooling only if the
context-specific initialization time is short compared to that of the
object’s constructor. But when context-specific initialization
is used, how does the object know it has been placed in a new
context? How does it object know when it has been returned to the
pool? It knows by implementing the
IObjectControl
interface, defined as:
interface IObjectControl : IUnknown { HRESULT Activate( ); void Deactivate( ); BOOL CanBePooled( ); };
COM+ automatically calls the IObjectControl
methods at the appropriate times. Clients of your object don’t
ever need to call these methods.
COM+ calls the Activate( )
method each time the object is pulled from the pool to serve a
client—just after it is placed in the execution context, but
before the actual call from the client. You should put
context-specific initialization in the Activate( )
method. Activate( )
is your pooled object’s
wakeup call—it tells it when it is about to start serving a new
client. When using Activate( )
, you should ensure
that you have no leftovers in your object state (data members) from
previous calls, or from a state that was modified from interaction
with previous clients. Your object should be indistinguishable from a
newly created object. The state should appear as if the
object’s constructor was just called.
COM+ calls Deactivate( )
after the client releases the object, but before leaving the context.
You should put any context-specific cleanup code in
Deactivate( )
.
When object pooling is enabled, after calling the
Deactivate( )
method, COM+ invokes the
CanBePooled( )
method to let your object decide whether it wants to be recycled.
This is your object’s opportunity to override the configured
object pooling setting at runtime. If your object returns
FALSE
from CanBePooled( )
, the
object is released and not returned to the pool. Usually, you can
return FALSE
when you cannot initialize the
object’s state to that of a brand-new object, because of an
inconsistency or error, or if you want to have runtime fine tuning of
the pool size and the number of objects in it. In the most cases,
your implementation of CanBePooled( )
should be
one line: return TRUE;
, and you should use the
Component Services Explorer to administer the pool. Implementing
IObjectControl
is not required for a pooled
object. If you choose not to implement it and you enable object
pooling, your object is always returned to the pool after the client
calls Release( )
on it.
Figure 3-5 emphasizes the calling sequence on a
pooled object that supports IObjectControl
. It
shows when COM+ calls the methods of
IObjectControl
and when the object is part of a
COM+ context.
Finally, IObjectControl
has two abnormalities
worth mentioning: first, the interface contains two methods that do
not return HRESULT
, the required returned value
according to the COM standard of any COM interface.
IObjectControl's
second abnormality is that
only COM+ can invoke its methods. The interface is not accessible to
the object’s clients or to the object itself. If a client
queries for
the
IObjectControl
interface, QueryInterface( )
returns
E_NOINTERFACE
.
[1] If you are not familiar with UML activities diagrams, read UML Distilled by Fowler and Scott (Addison Wesley, 1997). Chapter 9 in that book contains a detailed explanation and an example.