- In .Net gibt es
- die lock-Anweisung
- und die Monitor-Klasse
um den Zugriff auf gemeinsame kritische Codeabschnitte durch nebenläufige Thread zu synchronisieren.
- Manchmal gibt es Anwendungsfälle wo der gleichzeitige lesende Zugriff erlaubt ist aber der schreibende Vorgang synchronisiert werden muss. Dies wird mit der Klasse ReaderWriterLock und ReaderWriterLockSlim bewerkstelligt.
- Zugriffe auf einfache Feldwerte sollten Sie mit der Interlocked-Klasse sichern.
Der Einsatz von Threadsperren birgt jedoch die Gefahr des Auftretens von Deadlocks. Das Risiko ist umso höher je größer die Codeabschnitte und tiefer die Aufrufstapel sind, die ein Threadsperre umfasst. Daher sollten Sie bei der Programmierung immer daran denken, die "critical sections" so kurz wie möglich zu halten.
Das Ärgerliche an Deadlocks ist, dass sie spät entdeckt werden und meistens dann, wenn die Software im Produktivbetrieb läuft. In dieser Situation hilft nur ein Neustart des Dienstes und dies ist je nach Einsatzgebiet oft eine sehr kritische Sache.
Aus diesem Grund gibt es die Klasse gip.core.datamodel.ACMonitor, die folgende Vorteile mit sich bringt:
- Präventive Erkennung von Deadlocksituationen während der Entwicklungsphase mittels Lock Hierarchien" (auch als "Lock leveling" bekannt).
- Auflösen von Deadlocks während der "Reifephase" bzw. "Stabilisierungsphase" in Produktionsumgebungen.
Präventive Erkennung von Deadlocksituationen
Das Konzept der .NET-Monitor-Klasse sieht vor, dass man ein Feld von Typ object deklariert und beim Aufruf der Enter-Methode übergibt. Bei Verwendung der ACMonitor-Klasse übergeben Sie stattdessen ein ACMonitorObject. Der Konstruktor von ACMonitorObject erfordert die Übergabe eines int-Wertes, der den Lock-Level angibt. Beispiel:
public readonly ACMonitorObject _10020_LockValue = new ACMonitorObject(10020);
Die Nummer, die Sie hier vergeben sollte möglichst eindeutig sein und die Größe der Zahl der Anwendungsebene angepasst sein.
In iPlus ist es so organisiert, dass es eine fünfstellige Zahl ist. Die erste Stelle entspricht der Assembly, in der sie eingesetzt wird. Je kleiner die Zahl, desto tiefer ist die Anwendungsschicht:
Datalayer:
gip.core.datamodel: 10000
gip.mes.datamodel: 11000
Runtimelayer:
gip.core.autocomponent: 20000
Communicationlayer with other systems:
gip.core.communication: 30000
Managerlayer:
*.manager.dll: 40000
Applicationlayers:
*.processapplication*.dll: 60000
*bso*dll: 70000
Die zweite bis zur fünften Stelle dient zur weiteren Ebeneneinordnung innerhalb der Assemblies.
Wenn man nun die Lockobjekte verwendet, dann darf niemals ein Lockobjekt aus einer tieferen Ebene vor einem Lockobjekt aus einer höheren Ebene verwendet werden!
Damit dies nicht passiert benötigen Sie die ACMonitor-Klasse. Sie wird folgendermaßen verwendet:
using (ACMonitor.Lock(_11020_LockValue))
{
// Not allowed to use a lock with a higher number than 11020:
// SynchronizationLockException with be thrown in Debugger;
// otherwise the stacktrace dumped into the logfile
using (ACMonitor.Lock(_20010_LockValue))
{
}
}
Im obigen Fall werden Sie als Programmierer darauf hingewiesen werden, dass Sie die Lock-Hierarchie nicht eingehalten haben und dies eine potentielle Deadlockgefahr darstellt.
Die Benachrichtigung erfolgt auf zwei Arten:
Die Überprüfung der Lock-Hierarchien wird mittels "static bool ACMonitor.ValidateLockHierarchy" aktiviert. ValidateLockHierarchy wird entweder im Application-Config-File im Abschnitt CoreConfiguration aktiviert oder Sie setzen diese Eigenschaft direkt in Ihrem Anwendungscode.
Auflösen von Deadlocks
Mittels Lock-Hierarchien haben Sie bereits in der Entwicklungsphase das Risiko einer künftigen Deadlocksituation enorm verringert. Jedoch können Sie das dynamische Verhalten während der Entwicklungsphase niemals vollständig simulieren. Im Produktivbetrieb kommt vieles auf einmal zusammen:
- Viele Benutzer, die gleichzeitig über die iPlus-Netzwerkschicht mit den Prozessen interagieren,
- Kommunikation mit externen Systemen (Verschiedene Kommunikationsprotokolle, die in anderen Threads laufen und die Prozesszustände in den Anwendungsbäumen asynchron verändern)
- Andere Umgebungsbedingungen: Mehr Prozessorkerne und Memory; größere Datenmengen die zu verarbeiten sind; viele Geschäftsprozesse die gleichzeitig abzuarbeiten sind,...
- Unterschiedliche Muster von Aufrufstapeln aufgrund der serviceorientiertheit des iPlus-Frameworks. Das bedeutet, dass z.b. Ihr Programmcode von anderen Instanzen aufgerufen wird (ACComponents von anderen Herstellern), weil der Kunde zusätzliche Pakete erworben hat.
Mit dem Schalter "ValidateLockHierarchy" können Sie zwar im Produktivbetrieb Hierarchie-Verletzungen ausgeben, aber was passiert, falls dennoch ein Deadlock auftaucht?
Sie müssen den iPlus-Dienst im Taskmanger killen, weil die zwei beteilligten Threads nicht mehr weiterlaufen können und auch beim Herunterfahren "Thread.Join()" nicht abgeschlossen werden kann. Zudem kann eine abrupte Beendingung des iPlus-Dienstes fatale Folgen auf die Geschäftsprozesse (vorallem bei echtzeitfähigen industriellen Anlagen) haben. Hier muss dann abgewägt werden, welches Problem kritischer ist:
- Ein inkonsistenter Zustand einer ACComponent-Instanz, weil zeitgleich zwei Threads den kritischen Codeabschnitt durchlaufen wollten und eine Ausnahme stattgefunden hat, sodass es zu keinem Deadlock gekommen ist
- oder ein längerer Stillstand einer Produktionsanlage, bei der eventuell sogar Menschenleben in Gefahr sein könnten, weil der iPlus-Dienst steht?
Ihre Entscheidung ist hier bereits offensichtlich: Variante A - Sie nehmen den inkosistenten Zustand in Kauf, weil dieser durch die vielen Einflussmöglichkeiten über die Client-Oberfläche wiederhergestellt werden kann. Aktivieren Sie daher den Schalter "UseSimpleMonitor=false" für eine gewisse Zeit (z.B. für einige Wochen), damit das Gesamtsystem eine Reifephase oder Stabilisierungsphase durchlaufen kann.
Diese Einstellung führt dazu, dass im Falle eines Deadlocks eine SynchronizationLockException geworfen wird und Diagnose- und Stackinformationen im Meldungsprotokoll ausgegeben werden. Der Deadlock findet überhaupt nicht statt, sondern der Callstack der zur Verklemmung hätte führen können wird durch die Ausnahme aufgelöst. Der kritischen Codeabschnitte werden daher nicht vollständig durchlaufen und die entsprechende Instanz befindet sich dann in einem inkonsistenten Zustand. Die Ausgabe im Meldungsprotokoll muss hier unbedingt untersucht und an die zuständigen Entwickler weitergeleitet werden, damit das Programm abgeändert wird.
Ist das System einige Zeit deadlockfrei gelaufen (ohne Einträge im Meldungsprotokoll) setzen Sie UseSimpleMonitor auf true. Damit deklarieren Sie, dass die "Reifephase" abgeschlossen ist und das System nun stabil läuft. Diese Umstellung hat zur Folge, dass der rechenaufwändige Deadlock-Detektierungsmechnismus deaktiviert wird und stattdessen intern die effiziente Monitor-Klasse verwendet wird.