Singleton
Ich habe vor einigen Tagen in einem Weblog etwas über Singletons gelesen und mich daraufhin etwas weiter mit dem Thema des Singleton Pattern beschäftigt.
Hier ein kleines Tutorial von mir, für all diejenigen, die sich vielleicht ausgiebiger damit beschäftigen wollen…
Was und wofür ist ein Singleton gut?
Das Singleton (Einzelstück) ist ein in der Softwareentwicklung eingesetztes Entwurfsmuster und gehört zur Kategorie der Erzeugungsmuster (engl. Creational Patterns).
Es stellt sicher, dass zu einer Klasse nur genau ein Objekt erzeugt werden kann und ermöglicht einen globalen Zugriff auf dieses Objekt.
Das Muster gehört mit dem Iterator-Entwurfsmuster zu den bekanntesten der von der so genannten Viererbande[1] publizierten Muster.
Ein Singleton ist ein Objekt von dem es nur eine einzige Instanz in der gesamten Applikation gibt und von dem auch keine zusätzlichen Instanzen erzeugt werden können. Meist werden Singleton verwendet um über mehrere Methoden/Komponenten/Aufrufe hinweg irgendeine Art von Status zu halten. Prominente Beispiele sind etwa das Session-Objekt in ASP.NET oder auch Singleton-Server in .NET Remoting.
Verwendung
Das Singleton findet Verwendung, wenn
- nur ein Objekt zu einer Klasse existieren darf und ein einfacher Zugriff auf dieses Objekt benötigt wird oder
- wenn das einzige Objekt durch Unterklassenbildung spezialisiert werden soll.
Anwendungsbeispiele sind:
- Ein zentrales Protokoll-Objekt, das Ausgaben in eine Datei schreibt.
- Druckaufträge, die zu einem Drucker gesendet werden, sollten nur in einen einzigen Puffer geschrieben werden.
Das Singleton
- erzeugt und verwaltet einziges Objekt zu einer Klasse
- bietet globalen Zugriff auf dieses Objekt über eine Instanzoperation
- die Instanzoperation ist eine Klassenmethode, d.h. statisch gebunden
Vorteile
- Das Muster bietet eine Verbesserung gegenüber globalen Variablen.
- Das Singleton kann durch Unterklassenbildung spezialisiert werden.
- Sollten später mehrere Objekte benötigt werden, ist eine Änderung leicht möglich.
Nachteile
- In einigen objektorientierten Programmiersprachen gibt es keine Möglichkeit, Klassenmethoden zu beschreiben.
- Es besteht die Gefahr, durch exzessive Verwendung von Singleton quasi ein objektorientiertes Äquivalent zu globalen Variablen zu implementieren.
- In DLLs lassen sich Singletons nur eingeschränkt verwenden. Da DLLs nicht wie zum Beispiel Libraries zum Programm gelinkt werden, sondern von Haus aus gelinkt sind, wird ein Singleton, das in einer DLL und dem Hauptprogramm verwendet wird, in beiden Modulen ein eigenes Objekt sein. Das kann man (umständlich) vermeiden, indem das Hauptprogramm die eigene Instanz des Singleton an die Dll übergibt.
Eine erste einfache Implementierung
Singleton lösen vor allem eine zentrale Aufgabe: Sie verlagern die Objektinitialisierung vom Client in die Objektinstanz selber.
Nehmen wir ein einfaches Beispiel: Einen Counter. Diese Klasse könnte z.B. so aussehen:
public class SimpleCounter
{
public int Counter = 0;
public SimpleCounter() { }
}
Der Client wäre dann…
class Program
{
static void Test()
{
SimpleCounter sc = new SimpleCounter();
sc.Counter++;
Console.WriteLine(sc.Counter);
}
static void Test2()
{
SimpleCounter sc = new SimpleCounter();
sc.Counter++;
Console.WriteLine(sc.Counter);
}
static void Main(string[] args)
{
Test();
Test2();
}
}
Da sowohl die Methode Test() als auch Test2() jeweils mit einer eigenen Objektinstanz arbeiten, ist die Ausgabe in beiden Methoden ‚1‘ statt beim zweiten Aufruf eben weiterzuzählen. Der Client, in diesem Fall die Klasse ‚Program‘ kümmert sich selbst um die Objekterstellung.
Will man das in die Singleton-Klasse verlegen wird das Szenario kontrollierbarer :
public class SingletonCounter
{
public int Counter = 0;
protected SimpleCounter() { }
static SingletonCounter _instance = null;
public static SingletonCounter Instance()
{
if (_instance == null)
_instance = new SingletonCounter();
return _instance;
}
}
class Program
{
static void Test()
{
SingletonCounter sc = SingletonCounter.Instance();
sc.Counter++;
Console.WriteLine(sc.Counter);
}
static void Test2()
{
SingletonCounter sc = SingletonCounter.Instance();
sc.Counter++;
Console.WriteLine(sc.Counter);
}
static void Main(string[] args)
{
Test();
Test2();
}
}
Die Initialisierung erfolgt (dank protected Constructor) jetzt ausschließlich in unserer Klasse selbst, dort eben in der Methode Instance(). Diese Methode speichert eine eventuell neu erzeugte Objektinstanz in einer lokalen Variable die genauso wie die Methode selbst statisch und damit ohne vorherige Instanziierung zugreifbar ist.
Das Pattern würde sich übrigens auch dazu eignen, auf diese Art und Weise einen Objektpool zu verwalten, etwa indem einfach eine bestimmte Anzahl Objekte erzeugt werden kann etc.
Diese Implementation ist schon mal ganz nett, hat aber leider ein grundlegendes Problem: Sie kümmert sich nicht um die in multithreaded Szenarien vorkommenden konkurrierenden Zugriffe, ist also nicht Threadsafe.
Singleton threadsafe:
Zunächst müssen wir uns um die statische Zugriffsvariable kümmern. Was passiert bei einem Zugriff darauf durch Thread A während in Thread B gerade die Initialisierung durchgeführt wird?
Genau! C# hält für solche Fälle ein Schlüsselwort parat, mit dem man signalisieren kann, dass eine bestimmte Variable eventuell von mehreren Threads bearbeitet werden könnte: volatile. Auf derartig markierte Felder werden dann die Zugriffe serialisiert und ggf. durch sogenannte Memory Barriers bzw. Locks versehen.
Es gibt auch ein Attribut namens [IsVolatile()] das ggf. in VB zum Einsatz kommen kann (dort gibt es nämlich kein eigenes Schlüsselwort dafür).
MemoryBarrier()-Objekte geben dem Programmierer übrigens eigens für solche Aufgaben eine eigene API an die Hand [2].
Beim Thema Thread Safety ist man dann sehr schnell auch bei Locks. Die Grundidee von Locks ist einfach:
Ein Codeblock wird dabei markiert und wird ggf. exklusiv abgearbeitet. Ist also Thread A in der Ausführung innerhalb eines solchen gelockten Codeblocks, muß Thread B – falls er dasselbe tun möchte – ggf. warten bis Thread A fertig ist. Locks können auf zwei verschiedene Arten erzeugt werden:
1. Schlüsselwort Lock(x)
Locks müssen irgendwie unterscheidbar sein. Zu diesem Zweck wird ihnen in aller Regel eine Objektreferenz übergeben. Im Falle einer „normalen“ Variable kann das direkt die Variable sein, auf die zugegriffen wird, im Falle einer statischen Variable wird oft auch das Typenobjekt der Klasse selbst verwendet:
lock (typeof(SingletonCounter)) {
// ...
}
Es gibt eine ganze Reihe alternativer Optionen (incl. einer eigens zu diesem Zweck eingeführten Objektinstanz:
static readonly object syncRoot = new object()
lock( syncroot ) {
// ...
}
Der Witz ist, dass sichergestellt sein muß, dass alle Locks tatsächlich mit derselben Objektinstanz gelockt wurden.
Die Implementation von Brad Adams arbeitet mit einem eigenen statischen Sync-Objekt:
public sealed class Singleton {
private Singleton() {}
private static volatile Singleton value;
private static object syncRoot = new Object();
public static Singleton Value {
get {
if (Singleton.value == null) {
lock (syncRoot) {
if (Singleton.value == null) {
Singleton.value = new Singleton();
}
}
}
return Singleton.value;
}
}
}
Wer wissen möchte, weshalb lock( typeof( MyClass ) ) nicht immer ideal ist und wie er denn nun festlegt, welches Objekt er für seinen Lock idealerweise einsetzt, der findet hier eine ausführliche und lesenswerte Betrachtung dazu [3].
2. System.Threading.Monitor:
das Lock() Statement kapselt auch wieder Aufrufe in den System.Threading Klassen weg. thread(x) { } läßt sich einwandfrei übersetzen in
System.Threading.Monitor.Enter(x);
try {
...
}
finally {
System.Threading.Monitor.Exit(x);
}
Ein Monitor existiert theoretisch für jeden .NET Typ. Er kann jeweils nur angefordert werden, wenn er nicht gerade von einem anderen Thread durch den Aufruf von Enter() reserviert wurde und ist dann wieder verfügbar wenn Exit() final aufgerufen wurde. Es können mehrere Enter()-Aufrufe durch ein und denselben Thread gefeuert werden. Diese müssen dann auch wieder von genauso vielen Aufrufen zu Exit() beendet werden. Andere Threads dürfen jedoch keinen Lock „extern“ freigeben.
Und zu Schluss: Warum .NET einfach cool ist!
Wem jetzt nicht der Kopf schwirrt, der hat sich die Sachen noch nicht genau genug angesehen :)
Hier jetzt eine Threadsafe-Implementation eines einfachen Singleton-Counters wie es sein sollte:
sealed class SingletonCounter
{
public int Counter = 0;
private SingletonCounter(){}
public static readonly SingletonCounter Instance = new SingletonCounter();
}
Zunächst sorgen wir mit dem Schlüsselwort sealed dafür das die Klasse nicht beerbt werden kann. Der Konstruktor wird außerdem auf „Private“ gesetzt, wodurch er extern nicht mehr sichtbar und nur noch intern verwendbar ist. Der einzige statische Member, hier mit dem schönen Namen „Instance“ muß natürlich nach außen sichtbar sein und wird als readonly markiert um nachträgliche Änderungen an der Objektvariable auszuschließen. Die Initialisierung findet sozusagen bei erster Verwendung des Members im Code statt und ist danach fest „installiert“. Und da wir es hier mit einer statischen Variable zu tun haben kümmert sich das .NET Framework automatisch darum, daß Zugriffe darauf threadsafe durchgeführt werden. Ist das jetzt cool oder was?
Verwandte Entwurfsmuster
Die Eigenschaften des Singletones treffen für viele Klassen der anderen Muster zu, so dass diese dann als Singletone ausgeführt werden.
Zum Beispiel sind abstrakte Fabriken, Erbauer oder Prototypen oft auch Singletone.
Weiterführendes / Links / Quelle
[1] Viererbande: Mit der Viererbande (engl. Gang of Four, oft als GoF abgekürzt) sind Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides gemeint. 1995 veröffentlichten sie das Buch „Design Patterns – Elements of Reusable Object-Oriented Software“, ein Standardwerk im Bereich Software Engineering über Entwurfsmuster.
Der Grund für das Entstehen dieser Abkürzung ist angeblich, dass der Name des Buches und der Autoren zu lange sei um ihn in einer E-Mail zu zitieren. Deshalb wird er oft nur als „GoF book“ abgekürzt. In manchen Open-Source-Projekten finden sich Kommentare wie [GOF:175], die damit auf ein bestimmtes Entwurfsmuster in der englischen Ausgabe des Buches hinweisen (hier das Decorator-Muster).
Quelle: Wikipedia
[2] Brad Adams hat das in seinem Blog auch am Beispiel Singleton mit volatile beschrieben: volatile and MemoryBarrier() (Unbedingt auch die Kommentare anschauen!) Außerdem hat auf den Develop Mentor Maillinglisten Vance Morrison einmal ausgiebig über das Speichermodell in .NET und dem Singleton geschrieben.
(sehr empfehlenswert, besonders wenn man es etwas genauer wissen will)
Das Tutorial basiert zum größten Teil auf einem Weblog-Eintrag von Dirk Primbs „Das Singleton – das unbekannte Wesen“ und wude von mir überarbeitet und erweitert.