TL;DR:Mithilfe des Frameworks ABAP-Threads ist es möglich, Prozesse in ABAP durch ein einfaches Klassenkonzept zu parallelisieren. Dafür wird für jeden Thread im Hintergrund ein neuer Dialogprozess geöffnet, der die Programmlogik asynchron ausführt. So kann die Laufzeit von Reports deutlich verkürzt werden. Allerdings muss auf die Systemressourcen und andere Benutzer Rücksicht genommen werden, da es nur eine begrenzte Anzahl verfügbarer Dialogprozesse pro System gibt. Am besten eignet sich Parallelisierung zur Beschleunigung zeitkritischer Aufgaben außerhalb der Produktivzeit, wie beispielsweise Updates, für die der verfügbare Zeitraum ansonsten nicht ausreichen würde.
Parallelisierung von Programmen ist typischerweise kein Problem, mit dem sich ABAP-Entwickler häufig beschäftigen müssen. In den meisten Fällen haben Datenbankzugriffe durch OpenSQL einen deutlich größeren Einfluss auf die Performance eines Reports als die Verarbeitung und Ausgabe der Daten. Nichtsdestotrotz gibt es Szenarien, in denen Parallelisierung die Verarbeitungsgeschwindigkeit dramatisch beschleunigen kann. Um die prinzipielle Funktionsweise des Parallelisierungsprozesses zu demonstrieren, habe ich mir die Visualisierung des Mandelbrot-Fraktals ausgesucht. Das Mandelbrot-Fraktal ist wahrscheinlich der bekannteste Vertreter seiner Art und ein hervorragendes Beispiel für einen parallelisierbaren Prozess. Aber zuerst müssen die Grundlagen geklärt sein: Was genau ist Parallelisierung und welche Voraussetzung muss ein Prozess haben, um von Parallelisierung profitieren zu können?
Serielle und parallele Prozesse
Normalerweise werden ABAP-Programme und Reports seriell verarbeitet. Das bedeutet, das Programm wird Zeile für Zeile nacheinander abgearbeitet. Ein typischer Report liest Parameter vom Selektionsbildschirm ein, selektiert entsprechende Daten von der Datenbank, verarbeitet diese und gibt sie aus. Häufig werden Loops genutzt, um die Datenverarbeitung vieler Elemente zu realisieren. Das Programm arbeitet so alle Datenbankeinträge nacheinander ab, wie in der folgenden Grafik zu sehen ist:
Solche Loops sind ein gutes Indiz, dass Verarbeitungsschritte parallelisiert werden können. Die Voraussetzung hierfür ist, dass die Prozessschritte unabhängig voneinander sind, das heißt, ein Durchgang des Loops ändert nicht das Ergebnis eines anderen Durchgangs. Dies kann man zum Beispiel erzwingen, indem man separate Tabellen für die gelesenen Daten der Datenbank und für die Ergebnisse nutzt und dabei eine 1:1 Zuweisung macht. Der Ablauf eines parallelisierten Reports könnte wie folgt aussehen:
Da die im Bild dargestellten Verarbeitungsschritte alle zeitgleich erfolgen, wird die Programmlaufzeit dramatisch kürzer. Parallel ablaufende Programmteile werden auch Threads genannt. Leider lassen sich nicht alle Prozesse gleich gut parallelisieren. Einen Programmablauf zu konzipieren, der sich parallel verarbeiten lässt ist eine Wissenschaft für sich, aber hier sind ein paar Beispiele für besser und schlechter parallelisierbare Prozesse:
Besser Parallelisierbar | Schlechter Parallelisierbar |
---|---|
Für viele Personen das Gehalt berechnen | Für eine Person das Gehalt und die Lohnsteuer berechnen |
Für eine Person das Gehalt und Restanzahl Urlaubstage berechnen | Die Summe aller Gehälter der Mitarbeitenden berechnen |
ABAP-Threads
Als nächstes stellt sich die Frage, wie man in ABAP-Prozesse parallelisieren kann. Die Programmiersprache an sich stellt keine einfache Syntax zur Verfügung, die parallele Abarbeitung von Programmteilen zu beschreiben. An dieser Stelle kommt das Framework ABAP-Threads ins Spiel. Es ermöglicht eine parallele Verarbeitung über einen Mechanismus, der für jeden Thread einen neuen Dialogprozess im Hintergrund startet. Das bedeutet, dass die maximale Anzahl tatsächlich gleichzeitig ablaufender Threads von der Maximalanzahl von Dialogprozessen des Systems abhängt. Wenn man zu viele Threads startet, kann es auch passieren, dass für andere Benutzer keine Dialogprozesse mehr übrigbleiben. Das sollte man im Hinterkopf behalten. Im Zweifelsfall kann man sich über die Transaktion RZ11 den Parameter rdisp/wp_no_dia ansehen. Dieser enthält die Information über die maximale Anzahl von Dialogprozessen des Systems. Die Threads nutzen immer einen Dialogprozess, auch wenn der Report im Hintergrund als Batchprozess gestartet wurde.
ABAP-Threads lässt sich über ABAP-Git in das System einspielen. Hier ist der Link zum GitHub Repository: ABAP-Threads
Das Programmierkonzept von ABAP-Threads basiert auf sogenannten Workern. Für jeden Thread muss ein Worker-Objekt erzeugt werden. Dieses Objekt ist die Instanz einer Worker-Klasse, in der der parallel auszuführende Programmcode implementiert ist. Die Worker Klasse erbt von der abstrakten Klasse ZCL_THREAD_RUNNER_SLUG und hat die folgenden Kernkomponenten:
- Ein einziges Attribut (Membervariable), in der sowohl alle für die Teilaufgabe erforderlichen Parameter als auch die Ergebnisse gespeichert werden. Es stellt die Daten-Schnittstelle des Workers dar und sollte eine Struktur als Datentyp haben.
- Die Methode CREATE, die die Workerinstanz erzeugt und die Parameter in das Attribut kopiert. (Der Constructor kann aus technischen Gründen nicht genutzt werden)
- Die Methode GET_STATE_REF, welche die Referenz zum Attribut zurückliefert
- Die Methode RUN, welche den parallel auszuführenden Programmcode enthält
Das Attribut der Worker-Klasse stellt die einzige Schnittstelle für den Datenaustausch zwischen dem Hauptprogramm und dem Worker dar. Dadurch wird auch implizit sichergestellt, dass es keine Kommunikation zwischen den Worker-Instanzen geben kann. Diese Einschränkung hilft dabei, typische Fallstricke von paralleler Programmierung, wie beispielsweise Race-Conditions oder Deadlocks, zu umgehen, allerdings bedeutet sie auch, dass die Teilergebnisse der Worker nach der Verarbeitung gegebenenfalls in eine lokale Variable des Hauptprogramms kopiert werden müssen, was etwas Overhead mit sich bringt.
Der Programmablauf für die parallele Verarbeitung mithilfe von ABAP-Threads sieht dann typischerweise so aus wie in der Abbildung dargestellt:
Implementierung am Beispiel des Mandelbrot-Fraktals
Parallelisierungsstrategie
Um einen Prozess parallelisieren zu können, muss man eine Strategie entwickeln, ihn auf viele kleinere Teilprozesse aufzuteilen. Als erstes sollte man sich ansehen, welche Abhängigkeiten es im Prozess gibt. Beispielsweise wäre es nicht möglich, das Nettogehalt vor dem Bruttogehalt zu berechnen, da sich der Wert des Nettogehalts aus dem Bruttowert ergibt. Im Fall des Mandelbrot-Fraktals ist praktisch jeder Pixel des Bildes unabhängig von den anderen, deshalb gibt es viel Spielraum die Arbeit auf nahezu beliebig viele Worker aufzuteilen. In diesem Beispiel wird das Bild horizontal getrennt und auf die Worker aufgeteilt.
Die Bilddaten werden über eine Hilfsklasse erzeugt, welche eine Datei im Bitmap-Format generieren kann. Dabei ist die Farbe jedes Pixels direkt steuerbar und kann im Binärformat oder als HSV (Hue, Saturation, Value) beschrieben werden. Zum Algorithmus zur Darstellung des Mandelbrot-Fraktals gibt es dutzende Quellen im Internet.
Die Worker-Klasse
Als Datentyp des Attributs der Worker-Klasse wurde die Struktur t_data_and_parameters definiert. Sie enthält sowohl die Parameter, welche für die Bildgenerierung notwendig sind, als auch die Bilddaten des horizontalen Ausschnitts des Bildes. Die Definition befindet sich in der public section, da sie auch außerhalb der Klasse gebraucht wird.
Das Attribut selbst sollte dann in der private section definiert werden:
Die Methoden CREATE und GET_STATE_REF werden nach Schema redefiniert und implementiert. In der Redefinition der Methode RUN wird der Algorithmus ausprogrammiert, der anhand der Parameter im Attribut das Teilbild erzeugt und wieder im Attribut speichert. Dabei wird durch die Pixel der Bildzeilen iteriert und die Farbe jedes Pixels über eine Mandelbrot-Helferklasse bestimmt.
Der Report
Nachdem die Worker-Klasse fertig implementiert wurde, kann sie in einem Report genutzt werden. Dafür werden als erstes die Worker-Instanzen über einen Loop erzeugt. Anschließend können sie über die Methode RUN_PARALLEL gestartet werden. Um später den Zugriff zu vereinfachen, werden die Referenzen auf die Worker in einer Tabelle gespeichert. Im Codeausschnitt wird die Worker-Klasse LCL_PARALLEL_RENDERER genannt.
Jetzt muss gewartet werden, bis alle Worker fertig sind. Ein Worker hat seine Arbeit beendet, sobald er seine Methode RUN verlassen hat. Über die Methode IS_READY kann dies überprüft werden.
Anschließend können die Teilergebnisse zu einem Gesamtbild zusammengesetzt werden. Das Ergebnis ist eine vollständige Darstellung des Mandelbrot-Fraktals, das als Bitmap im SAP-GUI angezeigt werden kann:
Performance
Jetzt zum Interessantesten Teil: Wie viel Mehrleistung lässt sich durch die Parallelisierung erreichen? Um das zu beantworten, wurden mehrere Benchmarks auf einem Testsystem durchgeführt. Für jede Threadanzahl wurden 5 Messungen gemacht und der Durchschnitt gebildet. Das System läuft in einer virtuellen Maschine mit 24 V-Cores. Die Anzahl der Dialogprozesse ist auf 8 beschränkt, wobei einer für den Report selbst gebraucht wird.
Die Ergebnisse sind in den folgenden beiden Diagrammen dargestellt. Die Standardabweichung der Messwerte ist hier so vernachlässigbar klein, dass sie in der Darstellung nicht berücksichtigt wird.
Es ist deutlich zu erkennen, dass die Verarbeitungsgeschwindigkeit durch die Parallelisierung steigt. Das Verhältnis von Threadanzahl zu Performancesteigerung ist allerding nicht linear. Das liegt daran, dass die Parallelisierungsstrategie noch Verbesserungspotential hat. Der naive Ansatz, jedem Worker gleich viele Bildzeilen zuzuteilen ist nicht optimal, da der Algorithmus für die Generierung des Bildes rekursiv ist. Bildbereiche, die die schwarzen Pixel beinhalten, haben eine deutlich höhere Berechnungsdauer als der Rest. Der Flaschenhals ist derjenige Worker, der am längsten braucht. Aus diesem Grund steigt auch die Bearbeitungsgeschwindigkeit jenseits von sieben Workern. Auch, wenn immer nur sieben Worker gleichzeitig arbeiten, können die Threads, die bereits mit ihrem Bildausschnitt fertig sind, den nächsten Teil übernehmen. Der Overhead durch die Erstellung der Worker-Instanzen und das Kopieren der Bilddaten ist in diesem Beispiel vernachlässigbar klein.
Fazit
Abschließend kann gesagt werden, dass Parallelisierung mithilfe von ABAP-Threads eine gute Möglichkeit darstellt, die Verarbeitungsgeschwindigkeit von Prozessen erheblich zu steigern. Der Performancegewinn steht und fällt allerdings mit der Parallelisierungsstrategie. Außerdem sollte man Kenntnisse über die Systemhardware und Konfiguration der maximalen Anzahl an Dialogprozessen haben, um Konflikten mit den anderen Benutzern des Systems zu vermeiden.
Die Parallelisierung ist vorrangig für Offlineszenarien sinnvoll, wenn keine Last auf dem System ist. So können beispielsweise Updates oder zeitkritische Prozesse beschleunigt werden, die ansonsten möglicherweise im verfügbaren Zeitraum nicht möglich wären. Im Normalbetrieb eines Produktivsystems kann es allerdings andere Prozesse blockieren und die verfügbare Rechenleistung für andere Nutzer reduzieren, weshalb Parallelisierung hier mit Vorsicht zu genießen ist.