1. Problem
1.1. Context
You wish to 'ensure a class only has one instance and provide a global point of access to it.' [Gamma et al., 1994]
1.2. Summary
A single instance of a
class is required by a component or even the whole system. For example,
to provide a single point of access to a physical device, such as a
camera, or to a resource or resource pool, such as a logging object, a
thread or a block of memory.
A
way of synchronizing access to the single instance may be needed if you
wish it to be in scope for different parts of a system and used by
several threads or processes.
Instantiation
of the object should controllable. Instantiation should either be
deferred until the first time it is needed, or the object should be
instantiated in advance of other objects, to ensure a particular order
of initialization.
1.3. Description
This pattern is one of the simplest design patterns and arguably the most popular pattern found in [Gamma et al.,
1994]. Hence in this discussion, rather than examine the pattern in
itself, we place emphasis on how to implement it on Symbian OS, covering
a range of different circumstances. For detailed discussion of the
pattern itself covering, for example, the overall consequences of the
pattern or how to subclass a singleton class, please see [Gamma et al., 1994].
The pattern involves just one
class that provides a global point of access to a single instance,
which instantiates itself, either using Immortal (see page 53) or Lazy Allocation
(see page 63). However, there is a basic problem for developers working
on Symbian OS, which is its incomplete support for writable static data
in DLLs. To explain the issue, we must digress briefly: first of all,
to discuss what we mean by 'incomplete support for writable static data'
and then to explain why it's a big deal.
The Writable Static Data Restriction on Symbian OS
For the purposes of this
discussion, writable static data (WSD) is any modifiable globally-scoped
variable declared outside of a function and any function-scoped static
variable.
WSD was not allowed in DLLs built to run on target devices based on Symbian OS v8.1a or earlier.
For an explanation of how this limitation came about, see [Willee, Nov
2007] and [Stichbury, 2004]. WSD has always been allowed in EXEs but,
prior to Symbian OS v9, this was of little help to application
developers because applications were built as DLLs.
With the advent of Symbian
OS v9, the situation changed for the better. The use of WSD in target
DLLs is now possible. However, because it can be expensive in terms of
memory usage, it is not recommended when writing a shared DLL that is
intended to be loaded into a number of processes. This is because WSD in
a DLL typically consumes 4 KB of RAM for each process which loads the
DLL.
When a process loads its
first DLL containing WSD, it creates a single chunk to store the WSD.
The minimum size of a chunk is 4 KB, since this is the smallest possible
page size, so at least that amount of memory is consumed, irrespective
of how much static data is actually required. Any memory not used for
WSD is wasted.
Since the memory is per process, the potential memory wastage is:
- (4 KB − WSD bytes) × number of client processes
If, for example, a DLL
is used by four processes, that's potentially an 'invisible' cost of
almost 16 KB since the memory occupied by the WSD itself is usually in
the order of bytes.
Furthermore, DLLs that use
WSD are not fully supported by the Symbian OS emulator that runs on
Windows. Because of the way the emulator is implemented on top of
Windows, it can only load a DLL with WSD into a single emulated process.
If a second emulated process attempts to load the same DLL on the
emulator, it will fail with KErrNot-Supported.
There are a number of
occasions where the benefits of using WSD in a DLL outweigh the
disadvantages; for example, when porting code that uses WSD
significantly and for DLLs that will only ever be loaded into one
process. As a third-party developer, you may find the memory costs of
WSD acceptable if you create a DLL that is intended only to be loaded
into a single application. However, if you are working in device
creation, perhaps creating a shared DLL that ships within Symbian OS,
one of the UI platforms, or on a phone handset, the trade-offs are
different. Your DLL may be used by a number of processes and the memory
costs and constraints are such that using WSD is less likely to be
justifiable.
Application developers
are no longer affected as severely by the limitation, because a change
in the application framework architecture in Symbian OS v9 means that
all applications are now EXEs rather than DLLs. Writable static data has
always been allowed in EXEs built for target devices, so an application
developer can use WSD if it is required. Table 1 summarizes the support for WSD in DLLs and applications on various versions of the Symbian UI platforms.
Table 1. WSD Support Across Versions of Symbian OS and GUI Variants
UI Platform | Symbian OS version | WSD allowed in DLLs? | WSD allowed in applications? |
---|
UIQ 2.x | v7.0 | No | No |
S60 1st and 2nd Editions | v6.1, v7.0s, v8.0a, v8.1a |
UIQ 3 | v9.x | Yes, but not recommended due to the limitations on the Windows emulator and potentially large memory consumption. | Yes, since applications are EXEs. |
S60 3rd Edition |
Why Is the Limitation on WSD a Problem when Implementing Singleton?
Figure 1 shows the UML diagram for a typical Singleton.
The classic implementation of the Singleton pattern in C++ uses WSD, as shown below:
class Singleton
{
public:
static Singleton* Instance();
// Operations supplied by Singleton
private:
Singleton(); // Implementation omitted for clarity
~Singleton(); // Implementation omitted for clarity
private:
static Singleton* pInstance_; // WSD
};
/*static*/ Singleton* Singleton::pInstance_ = NULL;
/*static*/ Singleton* Singleton::Instance()
{
if (!pInstance_)
pInstance_ = new Singleton;
return (pInstance_);
}
Because of the constraint on the use of WSD, the classic implementation of Singleton cannot be used in DLLs before Symbian OS v9.
An alternative implementation, which we'll discuss shortly, must be
used instead. This alternative is also recommended when writing DLLs for
newer versions of Symbian OS that do allow WSD in DLLs, simply to avoid
unnecessary waste of memory and to enable complete testing on the
Windows emulator.
However, let's have another
brief digression and discuss the classic code above. As you can see,
the singleton object effectively owns and creates itself. The code
required to instantiate Singleton is private, so client code cannot create the singleton instance directly. Only the static function Instance(), which is a member of the class, can create pInstance_
and it does so the first time that it is called. This approach
guarantees the singularity of the object, in effect, enforcing it at
compile time, and also delays the construction until the object is
needed, which is known as Lazy Allocation
(see page 63). Using that pattern can be advantageous if an object is
'expensive' to instantiate and may not be used. A good example is a
logging class that is only used if an error occurs when the code is
running. There is no point in initializing and holding open a resource
such as a file server session if it is not needed when the code logic
runs normally.
Hazards of Singleton
This pattern is often
embraced for its simplicity by those new to design patterns, but it
presents a number of potential pitfalls to the unwary programmer,
especially the cleanup and thread-safety of the singleton class itself.
For example, we've just discussed the construction of Singleton, but what about the other end of its lifetime? Note that the destructor is private to prevent callers of Instance() from deleting pInstance_
accidentally or on purpose. However, this does raise some interesting
questions: When should the singleton instance destroy itself? How do we
de-allocate memory and release resource handles? [Alexandrescu, 2001] is
a good place to find out more and we'll discuss it in the Solution
section.
Also, how do we ensure that
the implementation is thread-safe if the singleton instance needs to be
accessed by more than one thread? After all, it's intended to be used as
a shared global resource, so it's quite possible that multiple threads
could simultaneously attempt to create or access the singleton. The
classical implementation we examined earlier is not thread-safe, because
race conditions are possible when separate threads attempt to create
the singleton. At worst, a memory leak arises if the first request for
the singleton by one thread is interrupted by the thread scheduler and
another thread then runs and also makes a request for the singleton
object.
Here again is the code for Instance(), with line numbers to aid the discussion:
1 /*static*/ Singleton* Singleton::Instance()
2 {
3 if (!pInstance_)
4 {
5 pInstance_ = new Singleton;
6 }
7 return (pInstance_);
8 }
Consider the following scenario: Thread A runs and calls Instance(). The singleton pInstance_ has not yet been created, so the code runs on to line 5, but it is then interrupted by the thread scheduler before pInstance_ can be created. Thread B starts executing, also calls Instance() and receives the same result as thread A when it tests the pInstance_
pointer. Thread B runs to line 5, creates the singleton instance and
continues successfully. However, when Thread A resumes, it runs from
line 5, and also instantiates a Singleton object, assigning it to pInstance_. Suddenly there are two Singleton objects when there should be just one, causing a memory leak because the one pInstance_ pointer references only the latter object.
When Singleton
is used with multiple threads, race conditions clearly need to be
avoided and we'll discuss how to do this in the Solution section below.
[Myers and Alexandrescu, 2004] provides an even more detailed analysis.
Should You Use Singleton?
Be wary of just reaching
for this pattern as there are a number of subtleties that can catch you
out later. This pattern increases the coupling of your classes by making
the singleton accessible 'everywhere', as well as making unit testing
more difficult as a singleton carries its state with it as long as your
program lasts. There are a number of articles that discuss this in more
depth but here are just a few:
Do we really need singletons? [Saumont, 2007]
Use your singletons wisely [Rainsberger, 2001]
Why singletons are evil [Densmore, 2004]
Synopsis
We've covered quite a lot of ground already, so let's summarize the problems that we need to address in the solution:
Symbian-specific issues
The classic implementation of Singleton does not allow for potential out-of-memory conditions or two-phase construction. What is the preferred approach in Symbian C++?
Singleton
cannot be implemented using the classic solution, which depends on WSD,
where WSD is not supported (in DLLs in Symbian OS v8.1a or earlier) or
is not recommended (DLLs in Symbian OS v9). How can you work around
this?
General issues
1.4. Example
On Symbian OS, the window server
(WSERV) provides a low-level API to the system's user interface devices –
screens, keyboard and pointer. The API is extensive and complex and
would make most application development unnecessarily convoluted if used
directly. For example, to receive events caused by the end user
interacting with a list box, you would need to use a window server
session, as well as Active Objects (see page 133), to handle asynchronous events raised by WSERV in response to input from the end user.
The control framework (CONE)
hides much of this complexity and provides a simplified API that meets
the requirements of most applications. CONE encapsulates the use of Active Objects (see page 133) and simplifies the interaction with the Window Server by providing a single, high-level class, CCoeEnv, for use by application programmers. CCoeEnv
also provides simplified access to drawing functions, fonts, and
resource files which are used by many applications. The class is also
responsible for creation of the cleanup stack for the application, for
opening sessions to the window server, file server, screen device,
application's window group and a graphics context.
Only one instance of CCoeEnv is needed per thread since it provides thread-specific information. However, CCoeEnv is intended for use by a potentially large number of different classes within an application, so the instance of CCoeEnv should be globally accessible within each thread.
2. Solution
Depending on the scope
across which you require the singleton to be unique there are a number
of different ways of working around the problem as shown in Table 9.3.
Some of the
implementations discussed are accompanied by their own issues. For
example, at the time of writing, Implementation C should be regarded as
illustrative rather than recommended on Symbian OS.
2.1. Implementation A: Classic Singleton in a Single Thread
Constraints on the Singleton
Table 2. Synopsis of Solutions
Implementation | Maximum Required Scope for the Singleton | Details | Requires WSD? |
---|
A | Within a GUI application or other EXEs such as console applications or servers | Implement normally with only minor modification for Symbian C++. | Yes |
B | Within a DLL for a specific thread | Use thread local storage (TLS). | No |
C | Across multiple threads | Use a thread synchronization mechanism to prevent race conditions. | Yes |
D | Across multiple threads | Manage initialization and access to the TLS data as each secondary thread starts running. | No |
E | Across multiple processes | Use the Symbian OS client–server framework. | No |
Details
On Symbian OS, the implementation of Singleton
must take into account the possibility of instantiation failure. For
example, a leave because there is insufficient memory to allocate the
singleton instance. One possible technique is to implement a standard NewL() factory function that will be familiar to Symbian C++ developers:
class CSingleton : public CBase
{
public:
// To access the singleton instance
static CSingleton& InstanceL();
private: // The implementations of the following are omitted for clarity
CSingleton();
~CSingleton();
static CSingleton* NewL();
void ConstructL();
private:
static CSingleton* iSingletonInstance; // WSD
};
/*static*/ CSingleton& CSingleton::InstanceL()
{
if (!iSingletonInstance)
{
iSingletonInstance = CSingleton::NewL();
}
return (*iSingletonInstance);
}
Inside InstanceL(), the NewL()
factory method is called to create the instance. The main issue is what
to do if this fails? One option would be to ignore any errors and pass
back a pointer to the singleton that might be NULL. However, this puts the burden on clients to check for it being NULL. They are likely to forget at some point and get a KERN-EXEC 3
panic. Alternatively, we could panic directly if we fail to allocate
the singleton which at least tells us specifically what went wrong but
still takes the whole thread down which is unlikely to be the desired
outcome. The only reasonable approach is to use Escalate Errors (see page 32) and leave (see Figure 2).
This means the client would have to deliberately trap the function and
willfully ignore the error to get a panic. While this is entirely
possible, it won't happen by accident. It also gives the client the
opportunity to try a less severe response to resolving the error than
terminating the thread.
Note: On most versions of Symbian OS, the tool chain disables the use of WSD in DLLs by default. To enable WSD you must add the EPOCALLOWDLLDATA
keyword to your MMP file. However, there are some builds of Symbian OS
(for example, Symbian OS v9.3 in S60 3rd Edition FP2) that allow WSD in
DLLs without the need to use the EPOCALLOWDLLDATA keyword. In addition, the currently supported version of the GCC-E compiler has a defect such that DLLs with static data may cause a panic during loading. The issue, and how to work around it, is discussed further in [Willee, Nov 2007].
A variant of this
approach is to give clients of the singleton more control and allow them
to instantiate the singleton instance separately from actually using it
(see Figure 3), allowing them to choose when to handle any possible errors that occur.
A separate method can then be supplied to guarantee access to the singleton instance once it has been instantiated.
class CSingleton : public CBase
{
public:
// To create the singleton instance
static void CreateL();
// To access the singleton instance
static CSingleton& Instance();
private: // The implementations of the following are omitted for clarity
static CSingleton* NewL();
CSingleton();
~CSingleton();
void ConstructL();
private:
static CSingleton* iSingletonInstance; // WSD
};
/*static*/ void CSingleton::CreateL()
{
// Flags up multiple calls in debug builds
ASSERT(!iSingletonInstance);
// Create the singleton if it doesn't already exist
if (!iSingletonInstance)
{
iSingletonInstance = CSingleton::NewL();
}
}
/*static*/ CSingleton& CSingleton::Instance()
{
ASSERT(iSingletonInstance); // Fail Fast (see page 17)
return (*iSingletonInstance);
}
This approach gives the
caller more flexibility. Only one call to a method that can fail is
necessary, in order to instantiate the singleton instance. Upon
instantiation, the singleton is guaranteed to be returned as a
reference, so removing the requirement for pointer verification or
leave-safe code.
Positive Consequences
Negative Consequences
2.2. Implementation B: TLS Singleton in a Single Thread
Constraints on the Singleton
Thread local storage (TLS)
can be used to implement Singleton where WSD is not supported or
recommended. This implementation can be used in DLLs on all versions of
Symbian OS, and in EXEs if desired. It is not thread-safe.
Details
TLS is a single storage area per thread, one machine word in size (32 bits on Symbian OS v9).
A pointer to the singleton object is saved to the TLS area and,
whenever the data is required, the pointer in TLS is used to access it.
The operations for accessing TLS are found in class Dll, which is defined in e32std.h:
class Dll
{
public:
static TInt SetTls(TAny* aPtr);
static TAny* Tls();
static void FreeTls();
...
};
Adapting Implementation A to use TLS does not change the API of CSingleton, except that the methods to create and access the singleton, CreateL() and Instance(),
must be exported in order to be accessible to client code outside the
DLL, as must any specific methods that the singleton class provides. The
implementation of CreateL() and Instance() must be modified as follows:
EXPORT_C /*static*/ void CSingleton::CreateL()
{
ASSERT(!Dll::Tls());
if (!Dll::Tls())
{ // No singleton instance exists yet so create one
CSingleton* singleton = new(ELeave) CSingleton();
CleanupStack::PushL(singleton);
singleton->ConstructL();
User::LeaveIfError(Dll::SetTls(singleton));
CleanupStack::Pop(singleton);
}
}
EXPORT_C /*static*/ CSingleton& CSingleton::Instance()
{
CSingleton* singleton = static_cast<CSingleton*>(Dll::Tls());
ASSERT(singleton);
return (*singleton);
}
Positive Consequences
Negative Consequences
The price of using
TLS instead of direct access to WSD is performance. Data may be
retrieved from TLS more slowly than direct access
through a RAM lookup. On ARMv6 CPU architectures, it is about 30 times
slower however this should be considerably improved by subsequent CPU
architectures.
Maintainability
is reduced because of the need to manage the single TLS slot per
thread. If TLS is used for anything else in the thread, all the TLS data
must be put into a single class and accessed as appropriate through the
TLS pointer. This can be difficult to maintain.
2.3. Implementation C: Classic Thread-safe Singleton Within a Process
Constraints on the Singleton
Must be used within a single process but may be used across multiple threads.
May be used within an EXE on any version of Symbian OS or in a DLL on Symbian OS v9.x, if absolutely necessary.
WSD may be used.
Details
We earlier demonstrated that the Instance() method of the classical implementation of Singleton is not thread-safe – race conditions may occur that result in a memory leak by creating two instances of the class.
At first sight, there appears to be a trivial solution to the problem of making Singleton leave-safe – add a mutex. However, consider the following pseudocode:
/*static*/ Singleton& Singleton::Instance()
{
Mutex.Lock(); // Pseudocode
if (!pInstance_)
{
pInstance_ = new Singleton();
}
Mutex.Unlock(); // Pseudocode
return (*pInstance_);
}
The use of Mutex has prevented the potential for race conditions and ensures that only one thread executes the test for the existence of pInstance_
at any one time. However, it also means that the mutex must be locked
and unlocked every time the singleton is accessed even though the
singleton creation race condition we described earlier can only occur
once. The acquisition of the mutex lock, and its release, results in an
overhead that is normally unnecessary. The solution may have looked
straightforward, but the overhead is far from desirable.
To work around this, perhaps we could only lock the mutex after the test for the existence of pInstance_? This gives us the following pseudocode:
/*static*/ Singleton& Singleton::Instance()
1 {
2 if (!pInstance_)
3 {
4 Mutex.Lock(); // Pseudocode
5 pInstance_ = new Singleton();
6 Mutex.Unlock(); // Pseudocode
7 }
8 return (*pInstance_);
9 }
However, this has reintroduced the possibility of a race-condition and thus a memory leak. Assume thread A runs and tests pInstance_ in line 2. If it is NULL,
the code must lock the mutex and create the singleton. However, suppose
thread A is interrupted, prior to executing line 4, by thread B. The
singleton hasn't been created yet, so thread B passes through the if
statement and on to line 4, locking the mutex and creating pInstance_,
before unlocking the mutex in line 6. When thread A runs again, it is
from line 4, and it proceeds to lock the mutex and create a second
instance of the singleton. The check for the existence of pInstance_ comes too early if it occurs only before the lock.
This gives rise to an implementation that double checks the pInstance_ pointer: that is, checking it after acquiring the lock as well as before. This is known as the Double-Checked Locking Pattern (DCLP) and was first outlined in [Schmidt and Harrison, 1996]:
/*static*/ Singleton& Singleton::Instance()
1 {
2 if (!pInstance_)
3 {
4 Mutex.Lock(); // Pseudocode
5 if (!pInstance_)
6 {
7 pInstance_ = new Singleton();
8 }
9 Mutex.Unlock(); // Pseudocode
10 }
11 return (*pInstance_);
12 }
Now the mutex is only acquired if pInstance_
is not yet initialized, which reduces the run-time overhead to the
minimum, but the potential for a race condition is eliminated, because a
check is also performed after the mutex is locked. It is these two
checks that give rise to the name of DCLP.
Now let's consider the simplest DCLP implementation in Symbian C++, using WSD. As we saw in Implementation A, there are two possible approaches:
Provide a method to provide access to the singleton instance and create it if it does not already exist.
Split
the creation of the singleton instance from the method required to
access it, to make it easier for calling code to use the singleton
without having to handle errors when memory resources are low.
The InstanceL() method for the former approach is as follows:
/*static*/ CSingleton& CSingleton::InstanceL()
{
if (!iSingletonInstance) // Check to see if the singleton exists
{
RMutex mutex;
// Open a global named RMutex (or RFastLock)
User::LeaveIfError(mutex.OpenGlobal(KSingletonMutex));
mutex.Wait(); // Lock the mutex
if (!iSingletonInstance) // Perform the second check
{
iSingletonInstance = CSingleton::NewL();
}
mutex.Signal(); // Unlock the mutex
}
return (*iSingletonInstance);
}
A very basic example of how the
singleton may be used is shown below. Three threads, a main thread and
two secondary threads, access the singleton, and call a method called DoSomething() to illustrate its use:
TInt Thread1EntryPoint(TAny* /*aParameters*/)
{
TRAPD(err, CSingleton::InstanceL().DoSomething());
...
return err;
}
TInt Thread2EntryPoint(TAny* /*aParameters*/)
{
TRAPD(err, CSingleton::InstanceL().DoSomething());
...
return err;
}
// Main (parent) thread code
// Creates a named global mutex
RMutex mutex;
User::LeaveIfError(mutex.CreateGlobal(KSingletonMutex));
CleanupClosePushL(mutex);
...
RThread thread1;
User::LeaveIfError(thread1.Create(_L("Thread1"), Thread1EntryPoint,
KDefaultStackSize, KMinHeapSize,
KMaxHeapSize, NULL));
CleanupClosePushL(thread1);
RThread thread2;
User::LeaveIfError(thread2.Create(_L("Thread2"), Thread2EntryPoint,
KDefaultStackSize, KMinHeapSize,
KMaxHeapSize, NULL));
CleanupClosePushL(thread2);
// Start the threads off 'at the same time'
thread1.Resume();
thread2.Resume();
TRAPD(err, CSingleton::InstanceL().DoSomething());
// Not shown here: Don't forget to clean up the threads, the mutex and
// the singleton
...
The alternative approach
described above splits creation of the singleton from access to it, to
make it easier for calling code to use. With the addition of DCLP, the
code for this approach looks as follows:
*static*/ CSingleton& CSingleton::Instance()
{
ASSERT(iSingletonInstance);
return (*iSingletonInstance);
}
/*static*/ void CSingleton::CreateL()
{
if (!iSingletonInstance) // Check to see if the singleton exists
{
RMutex mutex;
User::LeaveIfError(mutex.OpenGlobal(KSingletonMutex));
mutex.Wait(); // Lock the mutex
if (!iSingletonInstance) // Second check
{
iSingletonInstance = CSingleton::NewL();
}
mutex.Signal(); // Unlock the mutex
}
}
The code that uses the singleton is now more straightforward, since the Instance()
method always returns a valid instance and no errors have to be
handled. However, to be sure that the singleton has been created, each
thread must call the CSingleton::CreateL() method once, unless the thread is guaranteed to start after one or more other threads have already called CSingleton::CreateL().
Positive Consequences
Prevents race
conditions and provides a leave-safe solution for use where WSD is
available and multiple threads must be able to access a singleton.
Use of double-checked locking optimizes performance by locking the mutex on singleton creation only and not on every access.
Negative Consequences
4 KB of RAM is used for the WSD in the one process sharing the singleton.
There
is additional complexity of implementation and use compared to
Implementation A. If it's possible to ensure access to the singleton by a
single thread only, Implementation A is preferable.
Safe
use of DCLP cannot currently be guaranteed. There is no way to prevent a
compiler from reordering the code in such a way that the singleton
pointer becomes globally visible before all the singleton is fully
constructed. In effect, one thread could access a singleton that is only
partially constructed. This is a particular hazard on multiprocessor
systems featuring a 'relaxed' memory model to commit writes to main
memory in bursts in order of address, rather than sequentially in the
order they occur.
Symmetric multiprocessing is likely to be found in future versions of
Symbian OS. However, it is also a problem on single processor systems.
The 'volatile' keyword can potentially be used to write a safe DCLP
implementation but careful examination of the compiler documentation and
a detailed understanding of the ISO C++ specification is required. Even
then the resulting code is compiler-dependent and non-portable.
The
example given is insecure because it uses a globally accessible mutex.
Global objects are inherently prone to security and robustness issues
since a thread running in any process in the system can open them by
name and, in this case, block legitimate threads attempting to create
the singleton by calling Wait()Signal(). without ever calling
You
cannot share a singleton across multiple threads executing in different
processes. This is because each process has its own address space and a
thread running in one process cannot access the address space of
another process to get at the singleton object.
2.4. Implementation D: Thread-Managed Singleton within a Process
Constraints on the Singleton
Details
As its name suggests, the
storage word used by TLS is local to a thread and each thread in a
process has its own storage location. Where TLS is used to access a
singleton in a multi-threaded environment, the pointer that references
the location of the singleton must be passed to each thread. This is not
a problem since all threads are in the same process and they can all
access the memory address. This can be done using the appropriate
parameter of RThread::Create(). If this is not done, when the new thread calls Dll::Tls(), it receives a NULL pointer.
This implementation does not
need to use DCLP because, for the threads to share the singleton, its
instantiation must be controlled and the location of an existing
singleton passed to threads as they are created. The main thread creates
the singleton before the other threads exist, thus the code is
single-threaded at the point of instantiation and does not need to be
thread-safe. Once the singleton is available, its location may be passed
to other threads, which can then access it. Thus this implementation is
described as 'thread managed' because the singleton cannot be lazily
allocated, but must be managed by the parent thread.
Let's look at some code to clarify how this works. Firstly, the CSingleton
class exports two additional methods that are used by calling code to
retrieve the singleton instance pointer from the main thread, SingletonPtr(), and set it in the created thread, InitSingleton().
These methods are added so that code in a process that loads and uses
the singleton-providing DLL can initialize and use the singleton without
needing to be aware of how the singleton class is implemented.
class CSingleton : public CBase
{
public:
// To create the singleton instance
IMPORT_C static void CreateL();
// To access the singleton instance
IMPORT_C static CSingleton& Instance();
// To get the location of the singleton so that it can be passed to a
// new thread
IMPORT_C static TAny* SingletonPtr();
// To initialize access to the singleton in a new thread
IMPORT_C static TInt InitSingleton(TAny* aLocation);
private:
CSingleton();
~CSingleton();
void ConstructL();
};
EXPORT_C /*static*/ CSingleton& CSingleton::Instance()
{
CSingleton* singleton = static_cast<CSingleton*>(Dll::Tls());
return (*singleton);
}
EXPORT_C /*static*/ void CSingleton::CreateL()
{
ASSERT(!Dll::Tls());
if (!Dll::Tls()) // No singleton instance exists yet so create one
{
CSingleton* singleton = new(ELeave) CSingleton();
CleanupStack::PushL(singleton);
singleton->ConstructL();
User::LeaveIfError(Dll::SetTls(singleton));
CleanupStack::Pop(singleton);
}
}
EXPORT_C TAny* CSingleton::SingletonPtr()
{
return (Dll::Tls());
}
EXPORT_C TInt CSingleton::InitSingleton(TAny* aLocation)
{
return (Dll::SetTls(aLocation));
}
The code for the main thread
of a process creates the singleton instance and, as an illustration,
creates a secondary thread, in much the same way as we've seen in
previous snippets:
// Main (parent) thread must create the singleton
CSingleton::CreateL();
//Create a secondary thread
RThread thread1;
User::LeaveIfError(thread1.Create(_L("Thread1"), Thread1EntryPoint,
KDefaultStackSize, KMinHeapSize,
KMaxHeapSize,
CSingleton::SingletonPtr()));
CleanupClosePushL(thread1);
// Resume thread1, etc...
Note that the thread creation function now takes the return value of CSingleton::SingletonPtr() as a parameter value (the value is the return value of Dll::Tls() – giving access to the TLS data of the main thread). The parameter value must then be passed to CSingleton::InitSingleton() in the secondary thread:
TInt Thread1EntryPoint(TAny* aParam)
{
TInt err = CSingleton::InitSingleton(aParam);
if (err == KErrNone )
{ // This thread can now access the singleton in the parent thread
...
}
return err;
}
Positive Consequences
Negative Consequences
Accidental
complexity of implementation and use. For example, code that executes in
a secondary thread must initialize its own TLS data by explicitly
acquiring and setting the location of the singleton in the parent
thread.
The main thread must control the creation of every secondary thread.
This
solution works for multiple threads executing in a single process, but
cannot be used to share a singleton across multiple threads executing in
different processes. This is because each process has its own address
space and a thread running in one process cannot access the address
space of another process.
2.5. Implementation E: System-wide Singleton
Constraints on the Singleton
Details
As described in the
consequences sections of Implementations C and D, those implementations
allow a singleton to be accessed by multiple threads within a single
Symbian OS process, but not by multiple processes. The singleton
implementation is only a single instance within a process. If several
processes load a DLL which provides Implementation D, each process would
have its own copy of a singleton to preserve an important quality of
processes – that their memory is entirely isolated from any other
process.
Where a system-wide
singleton is required on Symbian OS, one process must be assigned
ownership of the singleton object itself. Each time another process, or
client, wishes to use the singleton it has to make an IPC request to get
a copy of the singleton. To enable this, the provider of the singleton
should make a client-side DLL available that exposes the singleton
interface to these other processes but is implemented to use IPC
transparently to obtain the copy of the actual singleton.
Having hidden the IPC details from clients behind what is effectively a Proxy [Gamma et al.,
1994] based on the singleton interface, you have a choice of which IPC
mechanism to use to transmit the copy of the singleton data. Here are
some mechanisms provided by Symbian OS that you can use to perform
thread-safe IPC without the need for WSD or the DCLP:
Client–Server (see page 182)
This
has been the most commonly used mechanism to implement this pattern as
it has existed in Symbian OS for many versions now. The server owns the
singleton and sends copies on request to its clients.
Publish and Subscribe kernel service
You
may find that if all you wish to do is provide a system-wide singleton
then using the Publish and Subscribe kernel service is easier to
implement. Note that whilst this is the same technology as used in Publish and Subscribe
(see page 114), the way it is used differs. For instance, whilst the
owning process does indeed 'publish' the singleton, the client-side DLL
does not 'subscribe' for notification of changes. Instead, it simply
calls RProperty::Get() when an instance of the singleton is needed.
Message Queues
Mentioned for completeness here as they could be used but the other alternatives are either more familiar or simpler to use.
The disadvantage of
having a system-wide singleton is the impact on run-time performance.
The main overhead arises from the context switch involved in the IPC
message to and from the process owning the singleton and, if the
singleton contains a lot of data, the copying of the singleton.
Positive Consequences
Allows access by multiple processes and thus provides a single, system-wide, instance.
The
Symbian OS IPC mechanisms manage the multiple requests for access to a
single shared resource and handle most of the problems of making the
singleton thread-safe.
Calling
code can be unaware of whether it is accessing a singleton – the
client-side implementation is simple to use, while the process supplying
the singleton can be customized as necessary.
Negative Consequences
Additional execution
time caused by the IPC and copying of the singleton data although any
implementation of Singleton that supports access from multiple processes
will also be affected by this.
The
implementation of the singleton is necessarily more complex, requiring
creation of a client-side DLL and use of an IPC mechanism. The
implementation can be heavyweight if only a few properties or operations
are offered by the singleton.
2.6. Solution Summary
Figure 4 summarizes the decision tree for selecting a singleton implementation on Symbian OS.
2.7. Example Resolved
The control environment (CONE) implements class CCoeEnv as a singleton class. Implementation B is used and a pointer to the singleton is stored in TLS. Internally, CCoeEnv accesses the singleton instance through a set of inline functions, as follows:
// coetls.h
class CCoeEnv;
inline CCoeEnv* TheCoe() { return((CCoeEnv*)Dll::Tls()); }
inline TInt SetTheCoe(CCoeEnv* aCoe) { return (Dll::SetTls(aCoe)); }
inline void FreeTheCoe() { Dll::FreeTls(); }
The constructor of CCoeEnv uses an assertion statement to confirm that the TLS slot returned by TheCoe() is uninitialized. It then sets the TLS word to point to itself, by calling SetTheCoe(this).
When an object of any class, instantiated within a thread that has a
control environment (i.e. the main thread of an application) needs
access to the CCoeEnv singleton, it may simply include the coemain.h header and call CCoeEnv::Static() which is implemented as follows in coemain.cpp:
EXPORT_C CCoeEnv* CCoeEnv::Static()
{
return(TheCoe());
}
It can then be used by applications as follows:
CCoeEnv::Static()->CreateResourceReaderLC(aReader, aResourceId);
Alternatively, CCoeControl::ControlEnv() can be used by applications to retrieve the singleton instance. However, this method returns a cached pointer to the CCoeEnv singleton, which the CCoeControl class stores in its constructor (by making a call to TheCoe()).
The use of a cached pointer is more efficient than calling CCoeEnv:: Static() because it avoids making an executive call to Dll::Tls() to access the singleton. Within the application framework, using a cached pointer is acceptable, because the lifetime of the CCoeEnv singleton is controlled and, if the application is running, the CCoeEnv object is guaranteed to exist (and to be at the same location as when the CCoeControl constructor first cached it).
In general, it is not
considered a good thing to cache pointers to singleton objects for
future access, because it makes the design inflexible. For example, the
singleton can never be deleted without providing some kind of mechanism
to ensure that those using a cached pointer don't access dead memory.
The Instance() method should generally be
used to access the singleton, because that allows more flexibility to
the provider of the singleton. For example, it could save on memory by
deleting the object if available memory drops below a certain threshold
or if it has not been accessed recently or regularly.
The implementations given
in the Solution section anticipate that some clients may mistakenly
cache a pointer to the singleton 'because they can' and instead return a
reference from Instance(), which is more difficult to code around (although it is still possible).
3. Other Known Uses
This pattern is widely used across Symbian OS since virtually every application makes use of the CCoeEnv singleton in its thread whilst every server is itself a singleton for the whole system.
4. Variants and Extensions
Destroying the Singleton
All
the implementations discussed have specified the destructor for the
singleton class as private. This is because, if the destructor was
public, it is possible that the singleton instance could be deleted,
leaving the singleton class to hand out 'dangling references' to the
deleted instance. Unless the design is such that this can be guaranteed
not to occur, it is preferable to prevent it.
Since
the singleton class has responsibility for creation of the instance, it
must generally have responsibility of ownership and, ultimately,
cleanup.
One option could
be to allow the singleton destruction to be implicit. C++ deletes
static objects automatically at program termination and the language
guarantees that an object's destructor will be called and space
reclaimed at that time. It doesn't guarantee the calling order, but if
there is only one singleton in the system, or the order of destruction
is unimportant, this is an option when Implementations A or C (using
WSD) are chosen. Please see [Vlissides, 1996] for more information about
this approach, which requires the use of a friend class to destroy the
singleton.
Another option is to use the atexit
function provided by the standard C library, to register a cleanup
function to be called explicitly when the process terminates. The
cleanup function can be a member of the singleton class and simply
delete the singleton instance. Please see [Alexandrescu, 2001] for
further details.
However,
you may want to destroy the singleton before the process terminates
(for example, to free up memory if the object is no longer needed). In
this case, you have to consider a mechanism such as reference counting
to avoid dangling references arising from premature deletion. For
example, to extend Implementation A, with the addition of a static Close() method:
class CSingleton : public CBase
{
public:
// To create the singleton instance
static void CreateL();
// To access the singleton instance
static CSingleton& Instance();
static void Close();
private: // The implementations of the following are omitted for
clarity
CSingleton* NewL();
CSingleton();
~CSingleton();
void ConstructL();
private:
static CSingleton* iSingletonInstance;
TInt iRefCount;
};
/*static*/ void CSingleton::CreateL()
{
++(iSingletonInstance->iRefCount);
if (!iSingletonInstance)
{ // Create the singleton if it doesn't already exist
iSingletonInstance = CSingleton::NewL();
}
}
/*static*/ CSingleton& CSingleton::Instance()
{
ASSERT(iSingletonInstance);
return (*iSingletonInstance);
}
// Must be called once for each call to CreateL()
/*static*/ void CSingleton::Close()
{
if (--(iSingletonInstance->iRefCount)<=0)
{
delete iSingletonInstance;
iSingletonInstance = NULL;
}
}