Are singletons ever appropriate? Is there a thread-safe way to create them? How can they be implemented? This article discusses these questions.
Introduction
Singletons need little introduction. A search on CodeProject alone turns up well over 50 articles about them.
So why another article? Well, various issues keep cropping up, and I wanted to weigh in on them. There are also some wrinkles regarding how the Robust Services Core (RSC) implements singletons, and I wanted to document them in an article. I'll try to keep this pithy.
Objections to Singletons
The Wikipedia entry on singletons links to several articles that consider them evil, and those articles make some points that you should consider when deciding whether to use a singleton.
- A singleton is like a global variable.
If access to the singleton's instance pointer is encapsulated, along with the singleton's data, what's the problem?
- A class shouldn't care if it's a singleton. This violates the single responsibility principle, so a factory should create the singleton instead.
If the class doesn't care after it's been created, then the factory is just boilerplate, put there to satisfy a religious edict. Almost every rule has exceptions that allow it to be broken.
- A singleton creates tight coupling between classes. Clients know it's a singleton, so you can't easily replace it with something polymorphic, for example.
If only one instance of a class is needed, this argument makes no sense.
There may also be times when using a singleton makes things easier, even though you know that multiple instances will eventually have to be supported. There is nothing wrong with this—provided that you have a plan for how to evolve your software. Good systems grow organically; trying to build everything right at the outset usually leads to failure.
- A singleton's state persists, which can make testing difficult.
Provide a way to reset or recreate the singleton.
- A singleton wastes memory or resources when no one is using it.
Use reference counting to destroy the singleton or release its resources. If this causes undue overhead, keep it or its resources around. Unless you have a lot of low usage singletons, it'll be much easier to just leave them allocated.
- Some languages don't readily support singletons.
This says something about those languages but nothing about the validity of singletons.
- Subclassing a singleton is almost impossible.
Then it shouldn't be a singleton.1
- In a multi-threaded environment, you might end up with multiple instances of a singleton.
Various articles discuss the need to lock when accessing a singleton. This adds a lot of overhead, which they try to minimize by only acquiring the lock when the singleton doesn't already exist. But even this is perilous, because simply accessing the singleton's instance pointer opens the door to race conditions. Maybe this can be solved by using an atomic variable, but it's not as easy as you would think….
Enough artificial complexity already! There's only going to be one instance of the singleton, so create it during system initialization, when only one thread ought to be running. If there are times when the singleton truly needs to be destroyed and recreated, assign this responsibility to a specific thread.
All of this has given us some guidelines for using singletons:
- Be sure that a single instance of the class will always be enough. If it won't be, have a plan for evolving your software to support multiple instances.
- Create singletons during system initialization.
- Consider providing a function that returns the singleton to its initial state to simplify testing.
- If destroying and recreating the singleton is a requirement, make a specific thread responsible for this.
Now we can look at how to implement singletons.
A Singleton Template
One benefit of templates is that they avoid the need to duplicate code. This confines changes to one place when implementing an enhancement or bug fix. The management of singletons falls into this category.
First, the introductory comments to the singleton template:
template<class T> class Singleton
{
};
RSC supports restarts, which are a way to partially reinitialize a system. To that end, it provides the memory types mentioned in the above comment. Each memory type is characterized by what types of restarts it survives and whether it is write-protected when the system is in service. We will now see the wrinkle that this introduces when managing singletons.
Here is the function that creates or accesses a singleton:
static T* Instance()
{
if(Instance_ != nullptr) return Instance_;
Debug::ft(Singleton_Instance());
if(Instance_ != nullptr) return Instance_;
Instance_ = new T;
auto reg = Singletons::Instance();
auto type = Instance_->MemType();
reg->BindInstance((const Base**) &Instance_, type);
return Instance_;
}
There are a few things to note here:
- This code is not thread safe. RSC primarily uses cooperative scheduling so that it rarely needs to protect critical regions at a granular level. If you're using this template outside RSC, you need to consider thread safety if you're not following the advice to create a singleton during initialization or from a specific thread.
- RSC provides a function trace tool that uses a singleton trace buffer. If the tool is enabled, invoking it (through
Debug::ft
) will, in itself, create that singleton, so the code must check to see if this occurred. - A restart frees memory by freeing the heap that provides the memory. Any singleton on such a heap disappears, so its instance pointer must be nullified. Rather than force every singleton to deal with this, the template adds each one to the global
Singletons
registry. The registry's primary responsibility is to nullify the instance pointers of each singleton that a restart will blow away.
Earlier, we noted that some singletons may want to support deletion:
static void Destroy()
{
Debug::ft(Singleton_Destroy());
if(Instance_ == nullptr) return;
auto singleton = Instance_;
auto reg = Singletons::Instance();
reg->UnbindInstance((const Base**) &Instance_);
Instance_ = nullptr;
delete singleton;
}
Again, a couple of things:
- The singleton must be removed from the
Singletons
registry. Destroy
is not invoked to delete a singleton during a restart. A restart just frees a heap without invoking the destructors of objects on that heap. This makes a restart much faster than it would otherwise be. If an object owns resources that need to be freed during a restart, it must provide a Shutdown
function to release them.
Next, a trivial function that can be very useful when resolving initialization order problems:
static T* Extant() { return Instance_; }
Now for the template's private
implementation details:
Singleton() { Instance(); }
~Singleton() { Destroy(); }
inline static fn_name
Singleton_Instance() { return "Singleton.Instance"; }
inline static fn_name
Singleton_Destroy() { return "Singleton.Destroy"; }
static T* Instance_;
Finally, the singleton's instance pointer—a static
member—needs to be initialized. Note that this must be done after (outside of) the class template:
template<class T> T* NodeBase::Singleton<T>::Instance_ = nullptr;
Although the Singletons
registry is also a singleton, it can't use the template because the Instance
function would try to add the registry to itself. It therefore clones the code that it needs from the template.
The Static Singleton
User megaadam commented that the following implementation, which is discussed on Stack Overflow, is thread safe as of C++11:
Singleton& Singleton::Instance()
{
static Singleton s;
return s;
}
There are a few places where RSC uses this to resolve initialization order problems. However, it doesn't support RSC's memory types, which the template does. It can only create the singleton in regular, pre-allocated memory, or on the default heap if modified to use new
. However, it is a useful technique to be aware of.
Usage
RSC uses singletons in various situations.
Registries. RSC has many registries, each of which tracks all the objects that derive from a common base class. Each registry is a singleton that provides access to its registrants using an identifier that distinguishes the various polymorphs. A template also implements much of a registry's behavior.
Flyweights. Many registries are populated by flyweights. Having more than one instance of a flyweight is very wasteful, so each is a singleton. For example, RSC's state machine framework defines the classes Service
, State
, and EventHandler
, all of whose leaf classes are flyweights that are placed in a registry. There is a global registry for services, and each service has registries for its states and event handlers. This supports a table-driven approach in which a service identifier, state identifier, and event identifier combine to look up and invoke the correct event handler.
Memory. Each heap and object pool is implemented by a singleton.
Threads. RSC has many singleton threads. Someday, a few of these will no longer be singletons, but for now, they still are:
RootThread
wraps the thread created for main
. InitThread
initializes the system and schedules threads once it is in service. CoutThread
front-ends all writes to cout
. CinThread
front-ends all reads from cin
. LogThread
spools logs to the console and a log file. FileThread
front-ends a file that is written to by more than one thread. CliThread
parses and executes commands entered through the CLI. StatisticsThread
generates periodic statistics reports. ObjectPoolAudit
returns a leaked memory block to its object pool. TimerThread
implements lightweight timers for state machines.
Notes
1 One benefit of writing an article is that it makes you revisit old code. The comments for my singleton template noted that a singleton's constructor and destructor should be private
—or protected
if subclasses had to be supported. This careless comment has now been revised.
History
- 25th November, 2020: Updated to mention thread safety and static singletons
- 22nd November, 2020: Initial version