3. Server-Activated Singleton
The server-activated singleton
activation mode provides a single, well-known object to all clients.
Because the clients connect to a single, well-known object, .NET ignores
the client calls to new, even
if the singleton object has not yet been created (the .NET runtime in
the client app domain has no way of knowing what goes on in the host app
domain anyway). The singleton is created when the first client tries to
access it. Subsequent client calls to create new objects and subsequent
access attempts are all channeled to the same singleton object (see Figure 3). Example 2
demonstrates these points: you can see from the trace output that the
constructor is called only once, on the first access attempt, and that obj2 is wired to the same object as obj1.
Example 2. A singleton object is created when first accessed, then used by all clients
public class MySingleton : MarshalByRefObject
{
int m_Counter = 0;
public MySingleton( )
{
Trace.WriteLine("MySingleton.MySingleton( )");
}
public void TraceCounter( )
{
m_Counter++;
Trace.WriteLine(m_Counter);
}
}
//Client-side code:
MySingleton obj1;
MySingleton obj2;
Trace.WriteLine("Before calling obj1 constructor");
obj1 = new MySingleton( );
Trace.WriteLine("After calling obj1 constructor");
obj1.TraceCounter( ); //Constructor will be called here
obj1.TraceCounter( );
Trace.WriteLine("Before calling obj2 constructor");
obj2 = new MySingleton( );
Trace.WriteLine("After calling obj2 constructor");
obj2.TraceCounter( );
obj2.TraceCounter( );
//Output:
Before calling obj1 constructor
After calling obj1 constructor
MySingleton.MySingleton( )
1
2
Before calling obj2 constructor
After calling obj2 constructor
3
4
|
Because the singleton
constructor is only called implicitly by .NET under the covers, a
singleton object can't have parameterized constructors. Parameterized
constructors are also banned because of an important semantic
characteristic of the singleton activation mode: at any given point in
time, all clients share the same state of the singleton object (see Figure 3).
If parameterized constructors were allowed, different clients could
call them with different parameters, which would result in a different
state for each client. If
you try to create a singleton object using a parameterized constructor,
.NET throws an exception of type RemotingException.
COM also supported
singletons by allowing you to provide a special class factory that
always returned the same object. The COM singleton behaved much like a
.NET singleton. Using ATL, designating a class as a singleton was done
by replacing the default class factory macro with the singleton macro.
The main difference between a COM singleton and a .NET singleton is that
with .NET, the object becomes a singleton because the host registers it
as such. Other hosts can register the same component type as a
single-call or client-activated object. With COM, the singleton was
always a singleton. |
|
3.1. Using singleton objects
Singleton
objects are the sworn enemy of scalability, because a single object can
sustain only so many concurrent client calls. Take care before deciding
to use a singleton object. Make sure that the singleton will not be a
hotspot for scalability and
that your design will benefit from sharing the singleton's object
state. In general, you should use a singleton object only if it maps
well to a true singleton in the application logic, such as a logbook to
which all components should log their activities. Other examples are a
single communication port or a single mechanical motor. Avoid using a
singleton if there is even the slightest chance that the business logic
will allow more than one such object in the future (e.g., if a second
communication port or another motor may be added). The reason is clear:
if your clients all depend on implicitly being connected to the
well-known object, and more than one object is available, the clients
will suddenly need to have a way to bind to the correct object. This can
have severe implications for the application's programming model.
Because of these limitations, I recommend that you generally avoid
singletons and instead find ways to share the state of the singleton,
instead of the singleton object itself. That said, there are cases when
using a singleton is a good idea; for example, class factories are
usually implemented as singletons.
3.2. Singleton object lifecycle
Once a singleton object
is created, it should live forever. That presents a problem to the .NET
garbage-collection mechanism, because even if no client presently has a
reference to the singleton object, the semantics of the singleton
activation mode stipulate that the singleton be kept alive so that
future clients can connect to it and its state. .NET uses leasing to
keep an object in a different process alive, but once the lease expires,
.NET disconnects the singleton object from the remoting infrastructure
and eventually garbage-collects it. Thus, you need to explicitly provide
the singleton with a long enough (or even infinite) lease.
A singleton object shouldn't provide a deterministic mechanism to finalize its state, such as implementing IDisposable.
If it's possible to deterministically dispose of a singleton object, it
will present you with a problem: once disposed of, the singleton object
becomes useless. Furthermore, subsequent client attempts to access or
create a new singleton will be channeled to the disposed object. A
singleton by its very nature implies that it's acceptable to keep the
object alive in memory for a long period of time, and therefore there is
no need for deterministic finalization. A singleton object should use
only a Finalize( ) method (the C# destructor).
It's important to
emphasize again that, in principle, you don't need to cross app domains
when using the different activation modes. As long as a proxy is present
between the client and the marshal-by-reference object, the client can
activate the object as single-call or singleton, even if it's in the
same app domain. In practice, however, you're likely to use the
server-activated single-call and singleton modes only on remote objects. |
|
4. Activation Modes and Synchronization
In a distributed
application, the hosting domain registers the objects it's willing to
expose, and their activation modes, with .NET. Each incoming client call
into the host is serviced on a separate thread from the thread pool.
That allows the host to serve remote client calls concurrently and
maximize throughput. The question is, what effect does this have on the
objects' synchronization requirements?
You can use
synchronization domains to synchronize access to remote objects, but
bear in mind that synchronization domains can't flow across app domains.
If a client creates a remote object that requires synchronization, the
object will have a new synchronization domain, even if the remote client
was already part of a synchronization domain. |
|
4.1. Client-activated objects and synchronization
Client-activated
objects are no different from classic client/server objects with respect
to synchronization. If multiple clients share a reference to an object,
and the clients can issue calls on multiple threads at the same time,
you must provide for synchronization to avoid corrupting the state of
the object. It would be best if the locking were encapsulated in the component
itself, by using either synchronization domains or manual
synchronization. The reason is clear: any client-side locking (e.g., via
the lock statement) locks only the
proxy, not the object itself. Another noteworthy point has to do with
thread affinity: because each incoming call can be on a different
thread, the client-activated object should not make any assumptions
about the thread it's running on and should avoid mechanisms such as
thread-relative static members or thread local storage. This is true
even if it's always the same client accessing the object and that client
always runs on the same thread.
4.2. Single-call objects and synchronization
In the case of a single-call
object, object-state synchronization isn't a problem, because the
object's state in memory exists only for the duration of that call and
can't be corrupted by other clients. However, synchronization is
required when the objects store state between method calls. If you use a
database, you have to either explicitly lock the tables or use
transactions with the appropriate isolation levels to lock the data. If
you use the filesystem, you need to prevent sharing of the files you
access while a call is in progress.
4.3. Singleton objects and synchronization
Unlike client-activated objects,
the clients of a singleton object may not even be aware they are
actually sharing the same object. As a result, synchronization of a
singleton object should be enforced on the object side. You can use
either a synchronization domain or manual synchronization. Like a client-activated object, a singleton object must avoid thread affinity.