Systemweiter Singleton in C#
Am 21. November, also vor gut einem Monat habe ich in meinem Artikel Singleton als vererbte Klasse am Beispiel C# gezeigt, wie das Singleton Entwurfsmuster so implementiert werden kann, dass eine Klasse lediglich von einer Singleton-Oberklasse erben muss, um nicht mehr als einmal in der aktuellen Applikationsdomäne instanziiert werden zu können. Wie versprochen möchte ich die damals Schritt für Schritt entwickelte Klasse heute so ausbauen, dass ein systemweiter Singleton realisiert werden kann. Dadurch kann von einer Klasse über die Applikationsdomäne hinaus nur ausschließlich eine Instanz existieren und wird es sehr einfach möglich, dass eine Anwendung beispielsweise nur einmal gestartet werden kann und jeder weitere Start lediglich das Hauptfenster in den Vordergrund holt.
Damit allerdings zwei Prozesse eines Systemes miteinander kommunizieren können, wird ein Datenkanal benötigt, über den dann ein Prozess von einem anderen ferngesteuert werden kann. Bitte behalten Sie also die folgende Illustration während des lesens im Hinterkopf, es wird Ihnen das Leben etwas erleichtern:
Bevor ich nun beginne, möchte ich kurz den letzten Stand in Erinnerung rufen: Wir entwickelten eine SingletonProvider Klasse, die alle Instanzen im eigentlichen Sinne verwaltet hat. Eine Vererbung fand im Anschluss daran über die Klasse SingletonBase statt:
SingletonProvider
using System;using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Reflection;
public static class SingletonProvider
{
private static Hashtable Selfs = new Hashtable();
private static object Lock = new Object();
public static T GetInstance<T>(out bool New)
where T: class // Jede Klasse ist erlaubt
{
// Auf öffentlichen Konstruktor prüfen
ConstructorInfo checkCtor = (typeof(T)).GetConstructor(Type.EmptyTypes);
/*
* Falls es einen solchen Konstruktor gibt,
* schmeißen wir einen Fehler, da es dem
* Charakter eines Singleton widerspricht.
*/
if (checkCtor != null)
throw new InvalidOperationException("Singleton means that you don't have any public constructors");
// Threadsynchronisation
lock (Lock) {
if (Selfs.ContainsKey(typeof(T).GUID))
{
// Instanz existiert bereits
New = false;
return (T)Selfs[typeof(T).GUID];
}
// Neue Instanz über Reflektion erstellen
ConstructorInfo ctorInfo;
// Geschützte Konstruktoren auslesen
ctorInfo = typeof(T).GetConstructor(
BindingFlags.NonPublic |
BindingFlags.Instance,
null,
Type.EmptyTypes,
null
);
// Konstruktor ohne Parameter aufrufen
T _Instanz = (T)ctorInfo.Invoke(new object[] { });
// Instanz der Hashtabelle zuführen
Selfs.Add(typeof(T).GUID, _Instanz);
New = true;
return (T)Selfs[typeof(T).GUID];
}
}
public static T GetInstance<T>()
where T: class
{
bool _Trash;
return GetInstance<T>(out _Trash);
}
}
SingletonBase
public abstract class SingletonBase<T>where T: SingletonBase<T>
{
public static T GetInstance(out bool New)
{
return SingletonProvider.GetInstance<T>(out New);
}
public static T GetInstance()
{
bool _Trash;
return GetInstance(out _Trash);
}
}
Um nun zunächst die SingletonProvider-Klasse auszubauen, verwende ich einen sogenannten Mutex1. Mutexe werden in aller Regel zur Prozesssynchronisation verwendet, das heißt, um sicherzustellen, dass kritische Programmabschnitte nicht gleichzeitig ausgeführt werden können, um beispielsweise Dateninkonsistenzen zu vermeiden. Sie lassen sich allerdings auch für unseren Zweck nutzen, da Mutexe systemweit registriert werden. Da sich die nötigen Klassen für Mutexe und unser folgendes Vorhaben aber in anderen Namensräumen (und Assemblies) befinden, müssen zunächst ein Verweis auf die Assembly System.Runtime.Remoting gesetzt - und die bekannten using-Direktiven um einige Namensräume ergänzt werden, so dass unsere Klassendefinition GlobalSingletonProvider künftig von den folgenden Zeilen angeführt wird:
using System;
using System.Threading;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting.Channels;
using System.Runtime.Serialization.Formatters;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.Remoting;
using System.Runtime.Serialization;
using System.Reflection;
public static class GlobalSingletonProvider
{ ... }
Dadurch aber müssen für jedes Singleton-Objekt nicht nur Instanzen verwaltet werden, sondern auch noch die zugehörigen Mutexe, die beim System registriert wurden. Hierzu wird unsere Liste von Deklarationen um eine Hashtabelle erweitert.
public static class GlobalSingletonProvider
{
private static Hashtable Selfs = new Hashtable();
private static Hashtable ApplicationMutexes = new Hashtable();
private static object Lock = new Object();
// ...
}
Im Gegensatz zum herkömmlichen SingletonProvider, wird im Rupf der Methode GetInstance() nun direkt nach der Überprüfung, ob bereits in der aktuellen Applikationsdomäne eine Instanz der Klasse existiert, ein neuer Mutex registriert, der als Namen die GUID, also eine absolut eindeutige ID der zu instanziierenden Klasse trägt. Eine boolsche Variable New gibt hier an, ob dem System ein solcher Mutex bereits bekannt ist. Das nämlich ist genau dann der Fall, wenn bereits außerhalb der Applikationsdomäne eine Instanz erstellt worden ist; der dort registrierte Mutex trägt nämlich aufgrund der ID der Klasse den selben Namen. Jetzt wird über sogenannte Ipc Channels ein Remote-Proxy auf dieses Objekt geholt, so dass das bestehende Objekt quasi "ferngesteuert" werden kann. Als Resultat dieser Hintergrundarbeit können Sie auf dem Objekt arbeiten, als hätten Sie es in Ihrer Applikationsdomäne erzeugt (Achtung: Das heißt nicht, dass sie auf dem Objekt arbeiten können, als hätten Sie es im gleichen Thread erzeugt. Beachten Sie Threadübergreifende Vorgänge!), alle Aufrufe werden in Wahrheit allerdings transparent weitergereicht. Kann im Gegensatz dazu der Mutex beim System neu registriert werden, lässt das darauf schließen, dass es sich um eine neue Instanz der Klasse handelt. Auch in diesem Fall wird ein Ipc Kanal bereitgestellt und darüber das Objekt verfügbar gemacht. Dieses Vorgehen bezeichnet man häufig auch als Marshalling2. Vorraussetzung für das Marshallen von Objekten ist allerings, dass diese Objekte von der Klasse MarshalByRefObject erben.
Das folgende Listing zeigt nun die bisherigen Modifikationen an der herkömmlichen SingletonProvider-Klasse, bereits bekannte Zeilen sind hier allerdings ausgegraut, damit deutlich wird, welche neuen Anweisungen unserem Vorhaben dienlich sind:
using System;
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Reflection;
using System.Threading;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting.Channels;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Remoting;
using System.Runtime.Serialization;
public static class GlobalSingletonProvider
{
private static Hashtable Selfs = new Hashtable();
private static Hashtable ApplicationMutexes = new Hashtable();
private static object Lock = new Object();
public static T GetInstance<T>(out bool New)
where T:
MarshalByRefObjekt // Jede Klasse ist erlaubt, die von MarshalByRefObjekt erbt
{
// Auf öffentlichen Konstruktor prüfen
ConstructorInfo checkCtor = (typeof(T)).GetConstructor(Type.EmptyTypes);
/*
* Falls es einen solchen Konstruktor gibt,
* schmeißen wir einen Fehler, da es dem
* Charakter eines Singleton widerspricht.
*/
if (checkCtor != null)
throw new InvalidOperationException("Singleton means that you don't have any public constructors");
// Threadsynchronisation
lock (Lock) {
if (Selfs.ContainsKey(typeof(T).GUID))
{
// Instanz existiert bereits
New = false;
return (T)Selfs[typeof(T).GUID];
}
// Mittels Mutex überprüfen, ob bereits eine Instanz existiert
ApplicationMutexes.Add(typeof(T).GUID, new Mutex(true, typeof(T).GUID.ToString(), out New));
// Ipc Kanal deklarieren
IpcChannel _Channel;
if (!New)
{
/*
* Falls in einer anderen Applikationsdomäne eine Instanz existiert,
* muss diese nun als Remote-Proxy bereitgestellt werden, dazu
* wird zunächst ein Ipc Channel registriert.
*/
SoapServerFormatterSinkProvider _ServProv = new SoapServerFormatterSinkProvider();
_ServProv.TypeFilterLevel = TypeFilterLevel.Full;
SoapClientFormatterSinkProvider _ClientProv = new SoapClientFormatterSinkProvider();
// Zur Serialisierung wird der Soap-Formatter verwendet
_Channel = new IpcChannel(null, _ClientProv, _ServProv);
ChannelServices.RegisterChannel(_Channel, false);
// Nun wird die Instanz über diesen Kanal geholt
T _Inst = (T)Activator.GetObject(typeof(T), "ipc://localhost:9190/GlobalSingleton_" + typeof(T).GUID.ToString());
Selfs.Add(typeof(T).GUID, _Inst);
}
else
{
/*
* Es existiert keine Instanz, also wird eine neue Instanz erstellt,
* der Mutex wird bei Applikationsende wieder freigegeben
*/
AppDomain.CurrentDomain.ProcessExit += delegate(object sender, EventArgs e)
{
try
{
((Mutex)ApplicationMutexes[typeof(T).GUID]).ReleaseMutex();
}
catch { /* Fehler ignorieren */ }
};
// Neue Instanz über Reflektion erstellen
ConstructorInfo ctorInfo;
// Geschützte Konstruktoren auslesen
ctorInfo = typeof(T).GetConstructor(
BindingFlags.NonPublic |
BindingFlags.Instance,
null,
Type.EmptyTypes,
null
);
// Konstruktor ohne Parameter aufrufen
T _Instanz = (T)ctorInfo.Invoke(new object[] { });
// Instanz der Hashtabelle zuführen
Selfs.Add(typeof(T).GUID, _Instanz);
// Ipc Kanal registrieren
SoapServerFormatterSinkProvider _ServProv = new SoapServerFormatterSinkProvider();
_ServProv.TypeFilterLevel = TypeFilterLevel.Full;
SoapClientFormatterSinkProvider _ClientProv = new SoapClientFormatterSinkProvider();
// Einstellungen für den Kanal werden in einer Hashtabelle abgelegt
IDictionary _Hashtable = new Hashtable();
_Hashtable["portName"] = "localhost:9190";
// Zur Serialisierung wird der Soap-Formatter verwendet
_Channel = new IpcChannel(_Hashtable, _ClientProv, _ServProv);
ChannelServices.RegisterChannel(_Channel, false);
// Objekt via Ipc Kanal "marshallen"
RemotingServices.Marshal((MarshalByRefObject)Selfs[typeof(T).GUID], "GlobalSingleton_" + typeof(T).GUID.ToString());
}
return (T)Selfs[typeof(T).GUID];
}
}
public static T GetInstance<T>()
where T: MarshalByRefObject
{
bool _Trash;
return GetInstance<T>(out _Trash);
}
}
Die Klasse GlobalSingletonBase unterscheidet sich kaum von der Klasse SingletonBase, mit Ausnahme, dass die neue Klasse nun von MarshalByRefObject erbt, so dass schlussendlich folgender Quelltext genau die Funktionalität bietet, die in der Einleitung erwähnt wurde:
using System.Collections.Generic;
using System.Text;
using System.Collections;
using System.Reflection;
using System.Threading;
using System.Runtime.InteropServices;
using System.Runtime.Remoting.Channels.Ipc;
using System.Runtime.Remoting.Channels;
using System.Runtime.Serialization.Formatters;
using System.Runtime.Remoting;
using System.Runtime.Serialization;
public static class GlobalSingletonProvider
{
private static Hashtable Selfs = new Hashtable();
private static Hashtable ApplicationMutexes = new Hashtable();
private static object Lock = new Object();
public static T GetInstance<T>(out bool New)
where T: MarshalByRefObjekt // Jede Klasse ist erlaubt, die von MarshalByRefObjekt erbt
{
// Auf öffentlichen Konstruktor prüfen
ConstructorInfo checkCtor = (typeof(T)).GetConstructor(Type.EmptyTypes);
/*
* Falls es einen solchen Konstruktor gibt,
* schmeißen wir einen Fehler, da es dem
* Charakter eines Singleton widerspricht.
*/
if (checkCtor != null)
throw new InvalidOperationException("Singleton means that you don't have any public constructors");
// Threadsynchronisation
lock (Lock) {
if (Selfs.ContainsKey(typeof(T).GUID))
{
// Instanz existiert bereits
New = false;
return (T)Selfs[typeof(T).GUID];
}
// Mittels Mutex überprüfen, ob bereits eine Instanz existiert
ApplicationMutexes.Add(typeof(T).GUID, new Mutex(true, typeof(T).GUID.ToString(), out New));
// Ipc Kanal deklarieren
IpcChannel _Channel;
if (!New)
{
/*
* Falls in einer anderen Applikationsdomäne eine Instanz existiert,
* muss diese nun als Remote-Proxy bereitgestellt werden, dazu
* wird zunächst ein Ipc Channel registriert.
*/
SoapServerFormatterSinkProvider _ServProv = new SoapServerFormatterSinkProvider();
_ServProv.TypeFilterLevel = TypeFilterLevel.Full;
SoapClientFormatterSinkProvider _ClientProv = new SoapClientFormatterSinkProvider();
// Zur Serialisierung wird der Soap-Formatter verwendet
_Channel = new IpcChannel(null, _ClientProv, _ServProv);
ChannelServices.RegisterChannel(_Channel, false);
// Nun wird die Instanz über diesen Kanal geholt
T _Inst = (T)Activator.GetObject(typeof(T), "ipc://localhost:9190/GlobalSingleton_" + typeof(T).GUID.ToString());
Selfs.Add(typeof(T).GUID, _Inst);
}
else
{
/*
* Es existiert keine Instanz, also wird eine neue Instanz erstellt,
* der Mutex wird bei Applikationsende wieder freigegeben
*/
AppDomain.CurrentDomain.ProcessExit += delegate(object sender, EventArgs e)
{
try
{
((Mutex)ApplicationMutexes[typeof(T).GUID]).ReleaseMutex();
}
catch { /* Fehler ignorieren */ }
};
// Neue Instanz über Reflektion erstellen
ConstructorInfo ctorInfo;
// Geschützte Konstruktoren auslesen
ctorInfo = typeof(T).GetConstructor(
BindingFlags.NonPublic |
BindingFlags.Instance,
null,
Type.EmptyTypes,
null
);
// Konstruktor ohne Parameter aufrufen
T _Instanz = (T)ctorInfo.Invoke(new object[] { });
// Instanz der Hashtabelle zuführen
Selfs.Add(typeof(T).GUID, _Instanz);
// Ipc Kanal registrieren
SoapServerFormatterSinkProvider _ServProv = new SoapServerFormatterSinkProvider();
_ServProv.TypeFilterLevel = TypeFilterLevel.Full;
SoapClientFormatterSinkProvider _ClientProv = new SoapClientFormatterSinkProvider();
// Einstellungen für den Kanal werden in einer Hashtabelle abgelegt
IDictionary _Hashtable = new Hashtable();
_Hashtable["portName"] = "localhost:9190";
// Zur Serialisierung wird der Soap-Formatter verwendet
_Channel = new IpcChannel(_Hashtable, _ClientProv, _ServProv);
ChannelServices.RegisterChannel(_Channel, false);
// Objekt via Ipc Kanal "marshallen"
RemotingServices.Marshal((MarshalByRefObject)Selfs[typeof(T).GUID], "GlobalSingleton_" + typeof(T).GUID.ToString());
}
return (T)Selfs[typeof(T).GUID];
}
}
public static T GetInstance<T>()
where T: MarshalByRefObject
{
bool _Trash;
return GetInstance<T>(out _Trash);
}
}
public abstract class GlobalSingletonBase<T> : MarshalByRefObject
where T: GlobalSingletonBase<T>
{
public static T GetInstance(out bool New)
{
return GlobalSingletonProvider.GetInstance<T>(out New);
}
public static T GetInstance()
{
bool _Trash;
return GetInstance(out _Trash);
}
}
Auf diesem Weg kann nun über Vererbung ein systemweiter Singleton genutzt werden. Das folgende Beispiel zeigt die Verwendung in der Praxis:
public class Start : GlobalSingletonBase<Start>
{
public Form WinForm;
private Start()
{ }
[STAThread]
static void Main(string[] args)
{
bool Created;
Start MainProcedure = Start.GetInstance(out Created);
// Applikation starten, falls es sich um die erste Instanz handelt
if (Created)
{
MainProcedure.WinForm = new HauptFormular();
MainProcedure.WinForm.Show();
Application.Run();
}
else
{
// Applikation läuft bereits, also in den Vordergrund holen
MainProcedure.WinForm.WindowState = FormWindowState.Maximized;
SetForegroundWindow(MainProcedure.WinForm.Handle);
}
}
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr Handle);
}
public class HauptFormular : System.Windows.Forms.Form
{
// ...
}
An dieser Stelle möchte ich mich für's lesen bedanken und hoffe, Ihnen an der ein oder anderen Stelle weitergeholfen zu haben. Dennoch bin ich mir bewusst darüber, dass einfach viele Dinge in diesem Artikel als bekannt vorrausgesetzt werden, dafür möchte ich mich entschuldigen. Sicher haben Sie Verständnis dafür, dass es sich nur um einen kurzen Abriss dessen handelt, was möglich ist und nicht um ein Handbuch und ich hoffe, dass bei Unklarheiten die Codebeispiele selbsterklärend sind. Ohne Programmierkenntnisse fällt es sicherlich schwer, alle Gedanken auf anhieb zu verstehen, ich kann Ihnen daher nur empfehlen, diese Beispiele schrittweise nachzuvollziehen.
1 Mutex: Wikipedia
2 Marshalling: Wikipedia
Bisherige Kommentare:

| Name: | Manuel |
| Datum: | 24.06.2008, 19:18 Uhr |
| URL: | http://www.josupeit.com |
| Kommentar: | Hallo Josef, vielen Dank für Deinen Kommentar. Nun, in der bereitgestellten Datei findet sich lediglich ein Beispiel dafür, wie eine Realisierung aussehen kann, in der Tat bin ich dort nicht auf einen solchen Sonderfall, wie den von Dir beschriebenen eingegangen. Wie steht es denn mit Deinem Lösungsansatz, wenn ein Prozess gekillt wird (und die AbandonedMutexException geworfen wird) im Detail? Kann der Mutex dann erneut registriert werden? Ich werde jedenfalls zusehen, dass ich auch Deinen Hinweis einarbeite und die Änderungen online stelle. Viele Grüße nach Österreich, Manuel |
| Name: | Hahnl Josef |
| Datum: | 24.06.2008, 09:51 Uhr |
| URL: | http://members.aon.at/hahnl |
| Kommentar: | Wird ein Programm, das Eigentümer des Mutex ist abgebrochen (z.B. Taskmanager) dann wird auch in der neuen Lösung AppDomain.CurrentDomain.ProcessExit bzw. auch AppDomain.CurrentDomain.DomainUnload NICHT ausgeführt. Für eine "saubere" Lösung stellt sich also die Frage, wie kann ein fremder Process ReleaseMutex()auf den Mutex ausführen??? Hilfreich eventuell auch folgende Methode: public static bool OtherInstanceRunning() { bool waitOne; try { // Signalisieren des Mutex und gleichzeitig abfragen, // ob bereits ein gleichnamiger Mutex existiert. waitOne = mutex.WaitOne(0, true); } catch (AbandonedMutexException) { // AbandonedMutexException ist neu in .NET Framework, Version 2.0. In früheren // Versionen wurde von der WaitOne-Methode true zurückgegeben, wenn ein Mutex // abgebrochen wurde. // Ein verwaister Mutex ist ein Hinweis auf einen schwerwiegenden Fehler im Code, // wird aber hier zum Feststellen eines gleichnamigen Mutex verwendet. // Die Wartezeit wurde abgeschlossen, weil ein Thread beendet wurde, ohne einen Mutex // freizugeben. Diese Ausnahme wird unter Windows 98 oder Windows Millennium Edition // nicht ausgelöst. waitOne = false; } return !waitOne; } Beste Grüße Josef |
| Name: | Manuel |
| Datum: | 15.05.2008, 14:15 Uhr |
| URL: | http://www.josupeit.com |
| Kommentar: | Eigentlich sollte der Mutex, der zur überprüfung, ob bereits eine Instanz registriert ist beim schließen der Anwendung wieder freigegeben werden, so dass das erste Programm beim Zugriff eine neue Instanz erzeugen sollte. Ich überprüfe das aber gerne nochmal. Wenn das entsprechende Objekt aber im Speicher gehalten werden soll, dann darf die entsprechende Instanz der Anwendung nicht geschlossen werden, das heißt, dass das entsprechende Singleton Objekt (und die Abhängigen Objekte) im Speicher gehalten werden müssen, bis alle anderen Instanzen geschlossen wurden. Ist aber nicht so einfach und ad hoc kann ich da auch keine Lösung präsentieren. Sorry. |
| Name: | Tom |
| Datum: | 15.05.2008, 12:55 Uhr |
| URL: | http:// |
| Kommentar: | Hallo Die Idee mit der systemweiten Singletonklasse ist super. Ich habe mit dem Code nur ein Problem. Wenn ich mehrere Programme welche die Singletonklasse benutzen starte, dann funktioniert alles bestens bis ich das Program welches die Singletonklasse als erstes aufgerufen hat schliesse. Anschliessend kriege ich bei allen anderen Programmen die auf die Klasse zugreiffen wollen eine Fehlermeldung :) Gibt es da Abhilfe? Wäre für einen guten Tipp dankbar. Gruss Tom |
Bisherige Trackbacks:
Keine











