Erweiterte Programmierung


Multihreading ist ein wichtiger Aspekt in der iPlus-Programmierung. Versuchen Sie immer möglichst Aufgaben zu parallelisieren aus folgenden Gründen:

  1. Aufrufe von Proxy-Komponenten zu reelen Komponenten (serverseitig) führen nicht dazu, dass der clientseitige Thread blockiert ist. Zum Beispiel, dass die Oberfläche auf Clientseite einfriert.
  2. In iPlus-Projekten sind oft viele Komponenten im Einsatz, die mit anderen Maschinen und Geräten im Netzwerk kommunizieren (z.B. SPS-Verbindungen per TCP-/IP, Modbus, OPC, http-basierende Dienste wie SOAP oder REST/JSON, FTP, SFTP, oder sonstige propriätere Protokolle). Instanzen aus den Anwendungsbäumen werden von diesen Kommunikationsinstanzen per Event-Technik informiert, wenn sich Werte oder Zustände geändert haben. Die Eventhandler in den Anwendungsinstanzen sollten hier Ihre Anwendungslogik an andere Threads delegieren, damit der Kommunikationsthread nicht blockiert wird. Lange Blockaden führen zum Stillstand der Oberfläche auf Clientseite und zum anderen müssen andere Anwendungsinstanzen lange warten bis sie informiert werden.
  3. Bessere Gesamtauslastung der Prozessoren.

Multihreaded Programming bedeutet umgekehrt, dass der Zugriff auf gemeinsame Resourcen per Threadsperren geschützt werden muss. Zudem birgt die Verwendung von Threadsperren die Gefahr von Deadlocks und das stellt für viele Programmierer eine besondere Herausforderung dar.

Im iPlus-Framework sind jedoch einige Mechanismen implementiert, die Ihnen helfen diese zuvor genannten Probleme und Herausforderungen leichter umzusetzen zu können:

  1. Verwendung der speziellen Klasse ACThread, um während der Laufzeit Performance-Analysen machen zu können.
  2. Einsatz von sogenannten "Lock Hierarchien" (auch als "Lock leveling" bekannt) mittels der Klasse ACMonitor und ACMonitorObject.
  3. Verwendung von ACDelegateQueue, um Aufgaben in eine Warteschlange zu stellen, die in seperaten Threads abgearbeitet werden.

 

 

Die Klasse "gip.core.datamodel.ACThread" ist eine Klasse, die eine .NET-Threadklasse kapselt. Verwenden Sie immer diese Klasse, damit die iPlus-Runtime in der Lage ist, Performance-Statistiken zu erstellen. Statistiken werden intern über eine statische Instanz der Klasse "gip.core.datamodel.PerformanceLogger" erstellt. Diese Thread-Statistiken geben Sie mit der Klasse RuntimeDump aus.

Wie man die ACThread-Klasse instanziiert und verwendet sehen Sie im folgenden Beispiel:

 

  1. Zuerst benötigen Sie eine Instanz von ManualResetEvent. ManualResetEvent wird benötigt, um einen Thread sauber beenden zu können, indem ein Signal vom Hauptthread an den Arbeitsthread gesendet wird, damit dieser seine Arbeits-Endlosschleife abbrechen kann (Siehe Punkt 4).
  2. Benötigen Sie eine Instanz von ACThread. Übergeben Sie bei der Instanziierung die Arbeitsmethode, die beim Starten des Threads aufgerufen werden soll und die eine Endlosschleife enthält in der die eigentliche Anwendungslogik ausgeführt werden soll. Im obigen Beispiel ist das die Methode RunWorkCycle().
  3. Erst in der ACInit-Methode starten Sie den neuen Thread. Sie sollten allerdings zuvor unbedingt die Name-Eigenschaft mit einem eindeutigen, für den Menschen lesbaren Identifizierer setzen. Verwenden Sie am besten immer die ACUrl der ACComponent-Instanz dafür.
  4. Fügen Sie in der Arbeitsmethode eine Endlosschleife ein, die so lange aktiv ist, bis die ManualResetEvent-Instanz ein Signal empfangen hat. Dazu rufen Sie innerhalb des While-Rumpfes die Wait-Methode auf. Entweder wartet die Wait-Methode die übergebene Wartezeit ab und kommt mit True zurück, oder sie kommt unmittelbar mit False zurück, wenn Sie vom Hauptthread ein Signal zur Terminierung erhalten hat.
  5. Innerhalb der Arbeitsschleife rufen Sie immer zuerst "StartReportingExeTime()" auf, um die Zeitmessung für die Threadstatistiken zu starten. StartReportingExeTime() gibt eine PerformanceEvent-Instanz zurück, die im Grunde genommen eine "System.Diagnostics.Stopwatch" ist.
  6. Danach kommt Ihr eigentlicher Applikationscode der im Beispiel oben durch die Methode DoSomething() symbolisiert ist.
  7. Am Schluss stoppen Sie die Zeitmessung "StopReportingExeTime()".
  8. Den Arbeitsthread beenden Sie in der ACDeInit-Methode indem Sie zuerst die Set-Methode der ManualResetEvent-Instanz aufrufen, um das Terminierungs-Signal an den Arbeitsthread zu senden.
  9. Rufen Sie danach die Join()-Methode auf (Intern wird "Thread.Join()" aufgerufen). Die Join-Methode wartet bis der Applikationscode im Arbeitsthread vollständig ausgeführt wurde und die Endlosschleife verlassen hat. Danach wird der Thread terminiert. Sollte die übergebene Timeout-Zeit nicht ausreichen, um den Applikationscode auszuführen, dann wird per Abort() die Terminierung forciert. In diesem Falle wird in das Meldungsprotokoll eine Warnmeldung geschrieben, dass die Join-Zeit nicht ausgereicht hat. In diesem Fall muss der Anwendungsentwickler die Ursache untersuchen und beheben. Sorgen Sie also dafür, dass Ihr Anwedungscode nicht blockiert ist, damit die Join()-Methode eine Thread auf regulärer Weise beenden kann.

 


Eigene Threads benötigen Sie immer dann, wenn Sie in gleichen Zeitabständen einen bestimmten Code zyklisch aufrufen möchten. Falls Sie jedoch unterschiedlichen Code haben, den Sie lediglich in einen anderen Thread delegieren möchten, damit der aufrufende Thread nicht blockiert wird, dann verwenden Sie die Klasse "gip.core.datamodel.ACDelegateQueue". ACDelegateQueue ist vergleichbar mit der Task-Klasse in ".NET". ACDelegateQueues haben jedoch den Vorteil, dass exakt immer nur ein expliziter ACThread verwendet wird und die Aufgaben in eine Queue gestellt werden, die in derselben Reihenfolge wieder abgearbeitet werden. Delegate-Queues können mittels RuntimeDump diagnostiziert werden, da durch die Verwendung von ACThread Performance-Statistiken erstellt werden können. Zudem können ACDelegateQueues auch gestoppt und neu gestartet werden (Methode "RestartQueue()").

ACDelegateQueue ist zudem die Basisklasse von Datenbankqueues die Sie im Datenbankkapitel bereits kennengelernt haben.

Im folgenden Beispiel sehen Sie wie Sie ACDelegateQueues verwenden:

 

  1. Instanziieren Sie in der ACInit()-Methode die ACDelegateQueue indem Sie einen eindeutigen Identifizierer übergeben. Wie auch bei ACThreads übergeben Sie am besten die ACUrl.
  2. Rufen Sie die Add()-Methode auf um ihren Delegaten zu übergeben.
  3. Rufen Sie StopWorkerThread() in der ACDeInit-Methode auf, um die Queue zu terminieren.

 


Der Arbeitsthread in einer ACDelegateQueue wird immer nur aktiv, sobald eine neue Aufgabe in die Queue gestellt wird. Die restliche Zeit verbringt er im schlafenden Zustand, damit nicht unnötig Rechenleistung verbraucht wird.

Falls Sie ACThread wie im ersten Beispiel verwenden möchten und nicht wollen, dass ständig die zyklische Arbeitsmethode "DoSomething()" aufgerufen wird (auch wenn es nichts zu tun gibt), dann verwenden Sie anstatt ManualResetEvent die Klasse gip.core.datamodel.SyncQueueEvents:

 

  1. Zuerst benötigen Sie eine Instanz von gip.core.datamodel.SyncQueueEvents. SyncQueueEvents wird benötigt, um einen Thread sauber beenden zu können, indem ein Signal vom Hauptthread an den Arbeitsthread gesendet wird, damit dieser seine Arbeits-Endlosschleife abbrechen kann (Siehe Punkt 4).
  2. Benötigen Sie eine Instanz von ACThread. Übergeben Sie bei der Instanziierung die Arbeitsmethode, die beim Starten des Threads aufgerufen werden soll und die eine Endlosschleife enthält in der die eigentliche Anwendungslogik ausgeführt werden soll. Im obigen Beispiel ist das die Methode RunWorkCycle().
  3. Erst in der ACInit-Methode starten Sie den neuen Thread. Sie sollten allerdings zuvor unbedingt die Name-Eigenschaft mit einem eindeutigen, für den Menschen lesbaren Identifizierer setzen. Verwenden Sie am besten immer die ACUrl der ACComponent-Instanz dafür.
  4. Fügen Sie in der Arbeitsmethode eine Endlosschleife ein, die so lange aktiv ist, bis die SyncQueueEvents-Instanz ein Signal empfangen hat. Dazu rufen Sie innerhalb des While-Rumpfes die WaitOne-Methode auf. Entweder wartet die WaitOne-Methode die übergebene Wartezeit von 0 Sekunden ab und kommt mit True zurück, oder sie kommt unmittelbar mit False zurück, wenn Sie vom Hauptthread ein Signal zur Terminierung erhalten hat.
  5. Rufen Sie die NewItemEvent.WaitOne()-Methode auf. Damit wird der Arbeitsthread schlafen gelegt.
  6. Erst wenn außerhalb von einem anderen Thread die NewItemEvent.Set()-Methode aufgerufen wird, wird der Arbeitsthread wieder aufgeweckt.
  7. Innerhalb der Arbeitsschleife rufen Sie immer zuerst "StartReportingExeTime()" auf, um die Zeitmessung für die Threadstatistiken zu starten. StartReportingExeTime() gibt eine PerformanceEvent-Instanz zurück, die im Grunde genommen eine "System.Diagnostics.Stopwatch" ist.
  8. Danach kommt Ihr eigentlicher Applikationscode der im Beispiel oben durch die Methode DoSomething() symbolisiert ist.
  9. Am Schluss stoppen Sie die Zeitmessung "StopReportingExeTime()".
  10. Den Arbeitsthread beenden Sie in der ACDeInit-Methode indem Sie zuerst die TerminateThread()-Methode der SyncQueueEvents-Instanz aufrufen, um das Terminierungs-Signal an den Arbeitsthread zu senden.
  11. Nachdem der Applikationscode im Arbeitsthread vollständig ausgeführt wurde und die Endlosschleife verlassen hat, rufen Sie ThreadTerminated() auf um dem Haupthread zu signalisieren, dass der Arbeitsthread seine Aufgaben erledigt hat.
  12. Die Join()-Methode auf (Intern wird "Thread.Join()" aufgerufen) wartet auf Schritt 11 und terminiert danach den Arbeitsthread.

  1. In .Net gibt es
    • die lock-Anweisung
    • und die Monitor-Klasse
      um den Zugriff auf gemeinsame kritische Codeabschnitte durch nebenläufige Thread zu synchronisieren.
  2. 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.
  3. 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:

  1. Präventive Erkennung von Deadlocksituationen während der Entwicklungsphase mittels Lock Hierarchien" (auch als "Lock leveling" bekannt).
  2. 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:

  1. Viele Benutzer, die gleichzeitig über die iPlus-Netzwerkschicht mit den Prozessen interagieren,
  2. Kommunikation mit externen Systemen (Verschiedene Kommunikationsprotokolle, die in anderen Threads laufen und die Prozesszustände in den Anwendungsbäumen asynchron verändern)
  3. Andere Umgebungsbedingungen: Mehr Prozessorkerne und Memory; größere Datenmengen die zu verarbeiten sind; viele Geschäftsprozesse die gleichzeitig abzuarbeiten sind,...
  4. 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:

  1. 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
  2. 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.