.NET supports two kinds of marshal-by-reference objects: client-activated and server- activated. The two kinds map to three activation modes: client-activated object, server-activated single-call, and server-activated singleton.
The different activation modes control object state management, object
sharing, the object lifecycle, and the way in which the client binds to
an object. The client decides whether to use client-activated or
server-activated objects. If the client chooses client-activated objects,
just one activation mode is available. If the client chooses
server-activated objects, it's up to the hosting app domain to decide
whether the client will get a server-activated single-call object or a
server-activated singleton object. These objects are called
server-activated because it's up to the host to activate the object on
behalf of the client and bind it to the client. The hosting app domain
indicates to .NET which activation modes it supports, using server
registration. The host can support both client- and server-activated
objects, or just one of these types; it's completely at the discretion
of the host. If it decides to support a server-activated mode, the host
must register its objects either as single-call objects or as singleton objects,
but not both.
1. Client-Activated Object
Client-activated
object mode is the classic client/server activation mode: when a client
creates a new object, the client gets a new object. That object is
dedicated to the client, and it's independent of all other instances of
the same class. Different clients in different app domains get different
objects when they create new objects on the host .
There are no limitations on constructing client-activated objects, and
you can use either parameterized constructors or the default
constructor. The constructor is called exactly once, when the client
creates the new remote object; if parameterized constructors are used,
.NET marshals the construction parameters to the new object. Clients can
choose to share their objects with other clients, either in their own
app domains or in other app domains. Like local objects,
client-activated objects can maintain state in memory. To make sure the
remote object isn't disconnected from the remoting infrastructure and
collected by its local garbage collector, client-activated objects
require leasing when
they make cross-process calls, to keep the objects alive for as long as
the clients are using them. Leasing, discussed later in this chapter,
provides a timestamp extending the life of the object.
Client-activated object mode
is similar to the default DCOM activation model, with one important
difference: the host app domain must register itself as a host willing
to accept client-activated calls before remote calls are issued. As
mentioned earlier, this means the process containing the host app domain
must be running before such calls are made.
2. Server-Activated Single Call
The fundamental
problem with the client-activated object mode is that it doesn't scale
well. The server object may hold expensive or scarce resources, such as
database connections, communication ports, or files. Imagine an
application that has to serve many clients. Typically, these clients
create the remote objects they need when the client application starts
and dispose of them when the client application shuts down. What impedes
scalability with client-activated objects is that the client
applications can hold onto objects for long periods of time, while
actually using the objects for only a fraction of that time. If your
design calls for allocating an object for each client, you will tie up
such crucial limited resources for long periods and will eventually run
out of resources. A better activation model is to allocate an object for
a client only while a call is in progress from the client to the
object. That way, you have to create and maintain in memory only as many
objects as there are concurrent calls, not as many objects as there are
clients. This is exactly what the single-call activation mode is about:
when the client uses a server-activated single-call object, for each
method call .NET creates a new object, lets it service the call, and
then discards it. Between calls, the client holds a reference to a proxy
that doesn't have an actual object at the end of the wire. The
following list shows how single-call activation works; its steps are
illustrated in Figure 2.
The object executes a method call on behalf of a remote client.
When the method call returns, if the object implements IDisposable, .NET calls IDisposable.Dispose( )
on it. .NET then releases all references it has to the object, making
it a candidate for garbage collection. Meanwhile, the client continues
to hold a reference to a proxy and doesn't know that its object is gone.
The client makes another call on the proxy.
The proxy forwards the call to the remote domain.
.NET creates an object and calls the method on it.
2.1. Benefits of single-call objects
The obvious benefit of
using single-call objects is the fact that you can dispose of the
expensive resources the objects occupy long before the clients dispose
of the objects. By the same token, acquiring the resources is postponed
until they are actually needed by a client. Keep in mind that creating
and destroying the object repeatedly on the object side without tearing
down the connection to the client (with its client-side proxy) is a lot
cheaper than creating and disposing of the object altogether. Another
benefit is that even if the client isn't disciplined enough to
explicitly discard the object, this has no effect on scalability,
because the object is discarded automatically.
If the client does call IDisposable.Dispose( ) on the object, it has the detrimental effect of recreating the object just so the client can call Dispose( ) on it. This is followed by a second call to Dispose( ) by the remoting infrastructure. |
|
2.2. Designing a single-call object
Although in theory you can
use single-call activation on any component type, in practice, you need
to design the component and its interfaces to support the single-call
activation mode from the ground up. The main problem is that the client
doesn't know it's getting a new object each time it makes a call.
Single-call components must be state-aware;
that is, they must proactively manage their state, giving the client
the illusion of a continuous session. A state-aware object isn't the
same as a stateless object. In fact, if the single-call object were
truly stateless, there would be no need for single-call activation in
the first place. A single-call object is created just before every
method call and deactivated immediately after each call. Therefore, at
the beginning of each call, the object should initialize its state from
values saved in some storage, and at the end of the call, it should
return its state to the storage. Such storage is typically either a
database or the filesystem. However, not all of an object's state can be
saved as-is. For example, if the state contains a database connection,
the object must reacquire the connection at the beginning of every call
and dispose of the connection at the end of the call, or in its
implementation of IDisposable.Dispose( ).
Using single-call
activation mode has one important implication for method design: every
method call must include a parameter to identify the object whose state
needs to be retrieved. The object uses that parameter to gets its state
from the storage and not the state of another instance of the same type.
Examples of such identifying parameters are the account number for bank
account objects, the order number for objects processing orders, and so
on. Example 1 shows a template for implementing a single-call class. The class provides the MyMethod( ) method, which accepts a parameter of type Param (a pseudo-type invented for this example) that identifies the object.
Example 1. Implementing a single-call component
public class Param
{...}
public class MySingleCallComponent : MarshalByRefObject,IDisposable
{
public MySingleCallComponent( )
{}
public void MyMethod(Param objectIdentifier)
{
GetState(objectIdentifier);
DoWork( );
SaveState(objectIdentifier);
}
void GetState(Param objectIdentifier)
{...}
void DoWork( )
{...}
void SaveState(Param objectIdentifier)
{...}
public void Dispose( )
{...}
/* Class members/state */
}
|
The object then uses the identifier to retrieve its state and to save the state back at the end of the method call.
Another design constraint
when dealing with single-call objects has to do with constructors.
Because .NET re-creates the object automatically for each method call,
it doesn't know how to use parameterized constructors, or which
parameters to provide to them. As a result, a single-call object can't
have parameterized constructors. In addition, because the object is
constructed only when a method call takes place, the actual construction
call on the client side is never forwarded to the objects:
MySingleCallComponent obj;
obj = new MySingleCallComponent( ); //No constructor call is made
obj.MyMethod( );//Constructor executes
obj.MyMethod( );//Constructor executes
Single-call
activation clearly involves a trade-off between performance (the
overhead of reconstructing the object's state on each method call) and
scalability (holding on to the state and the resources it ties up).
There are no hard-and-fast rules as to when and to what extent you
should trade performance for scalability. You may need to profile your
system and ultimately redesign some objects to use single-call
activation and others not to use it. |
|
2.3. Applying the single-call mode
The single-call activation mode (see Example 10-3)
works well when the amount of work to be done in each method call is
finite, and there are no more activities to complete in the background
once a method returns. You should not spin off background threads or
dispatch asynchronous calls back into the object, because the object
will be disposed of once the method returns. Because the single-call
object retrieves its state from some storage on every method call,
single-call objects work very well in conjunction with a load-balancing
machine, as long as the state repository is some global resource
accessible to all machines. The load balancer can redirect calls to
different machines at will, knowing that each single-call object can
service the call after retrieving its state.
.NET Enterprise Services
offer a set of smart instance-management techniques for .NET serviced
components. One of those services is just-in-time activation
(JITA), which works much like single-call objects. JITA has a few
advantages over single-call objects, mainly the ability to combine it
with other Enterprise Services instance-management techniques (such as
object pooling).