Aus Linux-Magazin 04/2017

Nebenläufige Programme entwickeln mit Pony

© Melory, 123RF

Die statisch getypte und objektorientierte Programmiersprache Pony trabt auf eher abseitigen Pfaden, um mit sicherem und hochperformantem Code für nebenläufige Anwendungen ihre Fans zu beglücken.

Indem die noch junge Programmiersprache Pony [1], deren Entwicklung 2011 am Imperium College London begann, das Aktor-Modell und Capabilities verwendet, gehören Deadlocks und Data Races der Vergangenheit an. Der Artikel erklärt Pony anhand der Beispielanwendung aus Abbildung 1. Ist diese erfolgreich kompiliert, protokolliert sie in einem einzeiligen Report den Verbrauch an Farben bei Malerarbeiten.

Abbildung 1: Oben kompiliert Pony erfolgreich, unten gibt <code>blue-horses</code> den Farbverbrauch wieder.

Abbildung 1: Oben kompiliert Pony erfolgreich, unten gibt »blue-horses« den Farbverbrauch wieder.

Zunächst wärmt Abbildung 2 aber Probleme mit der nebenläufigen Programmierung unter C und C++ auf. Das hypothetische Programm lagert zur Performancesteigerung Aufgaben in konkurrierende Threads aus, die auf den geteilten Speicherbereich zugreifen. Damit die Threads die Daten nicht unkontrolliert bearbeiten, sie wechselseitig verfälschen und Data Races erzeugen, regeln Locks den Zugriff. Blöd nur, wenn die sich durch Programmierfehler ineinander verhaken und zu Deadlocks werden.

Abbildung 2: Das Lock gew&auml;hrt dem Thread oben links exklusive Schreibrechte.

Abbildung 2: Das Lock gewährt dem Thread oben links exklusive Schreibrechte.

Die bei der Implementierung auftretenden Probleme fasst Tabelle 1 sprachübergreifend zusammen. Der Vergleich mit Java ist obligatorisch, da die Programmiersprache einen der erfolgreichsten Verbesserungsansätze für C und C++ anbietet. Ein Vergleich mit Rust [2] passt ebenfalls, weil sich auch der Jungspund unter den Sprachen bemüht, sicheren und schnellen Code für nebenläufige Anwendungen zu produzieren.

Tabelle 1

Vergleich von Sprachfeatures

Sprachfeature

C/C++

Java

Rust

Pony

AoT Compilation

ja

nein1

ja

ja

Speichersicher

nein

ja

ja

ja

Typsicher

nein

ja2

ja

ja

Vermeidet Data Races

nein

nein

ja

ja

Vermeidet Deadlocks

nein

nein

nein

ja

Actor-basiert

nein

nein

nein

ja

1 offiziell ab Java 9 [3] , 2 ausgenommen das dynamische Laden von Klassen [4]

Sowohl Pony als auch Rust kompilieren Programmtexte “ahead of Time” zu ausführbarem Binärcode. Die Methode spart Rechenzeit, da die Sprachen Typkonflikte so vorab erkennen und ausschließen. Java kompiliert Quellcode dagegen meist erst in nicht ausführbaren Bytecode, den eine virtuelle Maschine ausführt und überwacht. Die Speicherverwaltung überlassen sowohl Pony als auch Java einem Garbage Collector, den Pony, weil eine VM fehlt, in die Laufzeitumgebung des Kompilats integriert.

Aktor-Modell

Dank der Aktoren arbeiten Programme unter Pony stets frei von Data Races und Deadlocks. Das Modell verzichtet, wie Abbildung 3 zeigt, auf einen geteilten Speicherbereich und braucht daher keine Locks. Da die Threads – die auch Aktoren heißen – intern rein sequenziell arbeiten, könnten Data Races theoretisch noch zwischen den konkurrierenden Aktoren auftreten. Doch diese tauschen Anwendungsdaten untereinander über asynchrone Mitteilungen aus. Dabei löscht der Absender vor dem Versand alle Referenzen auf veränderbare Daten, was auch in diesem Szenario Data Races erfolgreich verhindert.

Abbildung 3: Behaviours nehmen die asynchronen Nachrichten entgegen und speisen sie in den sequenziellen Arbeitsmodus des Aktors ein.

Abbildung 3: Behaviours nehmen die asynchronen Nachrichten entgegen und speisen sie in den sequenziellen Arbeitsmodus des Aktors ein.

Als Postfächer für eingehende Mitteilungen dienen Behaviours (asynchrone Funktionen), die Pony wie gewöhnliche Methoden aufruft, aber asynchron abarbeitet. So setzt der Actor seine Arbeit sequenziell fort. Das Aktoren-Modell stammt nicht von den Pony-Entwicklern, auch die Programmiersprachen Erlang, Io, D und Scala nutzen es.

Installation

Die aktuelle Release 0.10.0-a337127 von Pony installiert der Admin mit den Befehlen aus Listing 1 auf einem 64-Bit-Debian. Zeile 1 integriert die Paketquelle in die Paketquellenliste der Datei »/etc/apt/sources.list«. Zeile 2 aktualisiert im Anschluss Debians Paketdatenbank, Zeile 3 installiert Pony. Für die i386-Architektur gibt es kein Debian-Paket, hier ist manuelles Kompilieren angesagt.

Listing 1

Installation unter Debian 8

01 echo "deb https://dl.bintray.com/pony-language/ponyc-debian pony-language main" | tee -a /etc/apt/sources.list
02 apt-get update
03 apt-get install ponyc-release

Listing 2 zeigt die Verzeichnisstruktur des Beispielprogramms. Wechselt der Programmierer in das Projektverzeichnis, kompiliert er das Programm mit »ponyc« wie in Abbildung 1 zum ausführbaren Binärprogramm, das der Befehl »./blue-horses« startet.

Listing 2

Übersicht der .pony-Dateien im Beispiel

01 |- blue-horses
02   |- blue-horses
03   |- main.pony
04   |- painter.pony
05   |- primitives.pony
06   |- ressource.pony

Die Datei »main.pony« dient als Einstiegspunkt für jedes Pony-Programm. Es bildet über die Klasse aus der Datei »ressource.pony« Farbvorräte zu den Primitiven (Farben) aus »primitives.pony« und überreicht diese dem Actor vom Typ »Painter« aus »painter.pony«. Der verarbeitet die Farbmenge und meldet seinen Verbrauch in Form eines Reports an »main.pony« zurück (Abbildung 4).

Abbildung 4: Der Actor <code>Painter</code> verarbeitet die Farbmenge und schickt dann einen Report &uuml;ber den Verbrauch an <code>main.pony</code>.

Abbildung 4: Der Actor »Painter« verarbeitet die Farbmenge und schickt dann einen Report über den Verbrauch an »main.pony«.

Klassen

Anwendungsdaten fasst Pony in Klassen zusammen, wie es zum Beispiel die Klasse »Ressource« aus Listing 3 verdeutlicht. Diese speichert die Farbtypen und den jeweiligen Vorrat. Das Schlüsselwort »class« führt die Klasse in Zeile 1 ein, während die Zeile darunter das öffentliche Feld »name« vom benutzerdefinierten Typ »Color« deklariert, was Zeile 6 von Listing 4 wieder aufgreift. Dank des Schlüsselworts »let« lässt sich das Feld im Gegensatz zu dem mit »var« initiierten nur einmal beschreiben.

Listing 3

ressource.pony

01 class Ressource
02   let name: Color
03   var _amount: U64 = 0
04
05   new create(name': Color) =>
06     name = name'
07
08   fun level(): U64 =>
09     _amount
10
11   fun ref fill() =>
12     _amount = _amount + 1

Zeile 3 von Listing 3 definiert das private Feld »_amount« als vom Datentyp Integer und setzt seinen Wert auf 0. Solche privaten Felder tragen einen Unterstrich im Namenspräfix. Auf sie darf nur die besitzende Instanz der Klasse zugreifen. Den Konstruktor der Klasse »Ressource« leitet in Zeile 5 das Schlüsselwort »new« ein. Die Parameterliste folgt nach »create()«. Die Variable »name’« übernimmt eine Referenz auf ein Objekt vom Typ »Color«, die Zeile darunter speichert dann das Feld »name« dauerhaft als eine Instanz der Klasse.

Das Schlüsselwort »fun« leitet die Methodendeklarationen für »level()« und »fill()« ein (Zeilen 8 und 11). Steht hinter der Parameterliste noch ein Doppelpunkt, folgt diesem der Typ des Rückgabewerts. Für »level()« ist es der Integertyp »U64« des privaten Feldes »_amount«.

Der Wert des Ausdrucks in der letzten Zeile einer Methode bildet gewöhnlich ihren Rückgabewert. Mit »return« schließen Entwickler Methoden vorzeitig ab. Die Methode »fill()« (Zeile 11) inkrementiert den Wert von »_amount« um 1.

Aufgrund der Angabe »ref« in Zeile 11 verlangt die Methode von einer Referenz, die auf eine Instanz der Klasse »Ressource« verweist, Schreibrechte. Solche Rechte gewähren die so genannten Reference Capabilities aus dem übernächsten Abschnitt. Zuvor folgen noch ein paar Worte zu den klassenähnlichen Primitiven.

Die Primitive

Primitive sind Klassen ohne Felder. Für sie existiert immer nur eine Instanz, etwa wie für None und Null in anderen Sprachen. Listing 4 deklariert zunächst die methodenlosen Primitive »Amber«, »Blue«, »Crimsen« und »Other« symbolisch als Typen für die verwendeten Farben. Zeile 6 vereint die vier Primitive unter dem Typ-Alias »Color«.

Listing 4

primitives.pony

01 primitive Amber
02 primitive Blue
03 primitive Crimsen
04 primitive Other
05
06 type Color is (Amber | Blue | Crimsen | Other)
07
08 primitive Properties
09   fun list(): Array[Color] =>
10     [Amber, Blue, Crimsen, Other]
11
12   fun name(x: Color): String =>
13     match x | Amber => "amber" | Blue => "blue" | Crimsen => "crimsen" else "an other color" end

Das Primitiv »Properties« gruppiert hingegen Funktionalitäten in Form von Methoden, konkret die zwei Hilfsmethoden »list()« und »name()«. Die erste liefert alle Farben in einem Feld zurück, die zweite gibt für die übergebenen Primitive (»x«) den zugehörigen Namen der Farbe als Zeichenkette zurück. Die Auswahl in Zeile 13 nutzt den »match«-Ausdruck, um Farben zu identifizieren. Weitere zusammengesetzte Datentypen finden sich mit »set« und »map« in Ponys Standardbibliothek im Collections Package.

Reference Capabilities

Der Clou sind hier aber die Reference Capabilities (Tabelle 2), mit denen Pony Data Races verhindert. Mit ihnen vermeiden es Programmierer, mit zwei oder mehr Aktoren mehrerer Referenzen auf ein veränderbares Objekt zu setzen.

Tabelle 2

Reference Capabilities

Typ

Sendbar

Schreibrecht

Leserecht

iso

ja1

exklusiv

exklusiv

trn

nein

exklusiv

geteilt

ref

nein

geteilt

geteilt

val

ja

nein

geteilt

box

nein

nein

geteilt

tag

ja

nein

nein

1 aber erst nach dem Löschen im Sender

Reference Capabilities einzusetzen und mit den daraus resultierenden Effekten umzugehen bereitet zunächst etwas Mühe, Anwender brauchen vermutlich eine Gewöhnungsphase. Der Ausdruck »var picasso: Painter iso« würde zum Beispiel die Referenz »picasso« deklarieren, die auf ein Objekt vom Typ »Painter« verwiese. Dank der Capability »iso« erhält sie das exklusive Schreib- und Leserecht für das Objekt. Da »picasso« die einzige Referenz auf das Objekt ist, lässt es sich nach Löschen von »picasso« sicher an einen anderen Actor versenden.

Um sinnvolle Programme zu schreiben, braucht Pony auch die anderen Capabilities aus Tabelle 2. Objekte unter der Capability »val« sendet Pony wie beim Broadcast an alle Aktoren, denn diese Objekte sind unveränderlich. Mit »trn« und »box« existiert jeweils eine entsprechende Capability für die Datenverarbeitung im Actor. Die Capability »ref« agiert als Standard für Variablen und verhält sich wie eine Referenz in anderen Programmiersprachen, während »tag« Aktoren vor direktem Zugriff schützt.

Die Kommunikation erfolgt unter dem Aktoren-Modell ohnehin über den Aufruf von Behaviours. Die Programmiersprache Rust beschränkt ebenfalls Referenzen, um Data Races zu vermeiden. Deren Benutzerrecht [5] gestattet pro Objekt nur eine Referenz mit exklusivem Schreib- und Leserecht. Um sinnvolle Programme zu schreiben, verleihen Entwickler in diesem Fall das Exklusivrecht.

Aktoren

Aktoren orchestrieren die Pony-Anwendung. Einmal gestartet lauern sie auf asynchrone Nachrichten, werten die gelieferten Daten gemäß der eingebundenen Behaviours aus und senden die Resultate an andere Aktoren weiter. Listing 5 zeigt den Actor »Painter« aus der Beispielanwendung. Er konsumiert Farben und meldet seinen Verbrauch an den Actor »Main« weiter. Ein Actor ähnelt dabei einer Klasse, speichert aber zusätzlich die erwähnten Behaviours.

Listing 5

painter.pony

01 actor Painter
02   let listener: Main
03   var history:  Array[Ressource box] = Array[Ressource box]
04
05   new create(listener': Main) =>
06     listener = listener'
07
08   be paint(res: Ressource iso) =>
09     if res.level() > 0 then
10       history.push(consume res)
11       listener.notify(stats())
12     else
13       res.fill()
14       paint(consume res)
15     end
16
17   fun stats(): String =>
18     var msg: String = "the painter has consumed: "
19     for x in Properties.list() do
20       var sum: U64 = 0
21       for y in history.values() do
22         sum = sum + if y.name is x then y.level() else 0 end
23       end
24       msg = msg + Properties.name(x) + ": " + sum.string() + ";  "
25     end
26     msg

Der Konstruktor des Aktors »Painter« übernimmt in Zeile 5 eine Referenz des »Main«-Aktors (der aus Listing 6, Zeile 6) und speichert sie im Feld »listener«. Das Feld »history« (Listing 5, Zeile 3) notiert alle über »paint« zugesandten Objekte vom Typ »Ressource« in einem Feld. Die Zeilen 8 bis 15 definieren das Behaviour »paint«. Es verlangt ebenfalls ein Objekt vom Typ »Ressource«. Erweist sich der Füllstand der Ressource in Zeile 9 größer als null, speichert das Feld »history« beim Aufruf der Methode »push()« aus Zeile 10 eine weitere Referenz. Da die Referenz »res« dank »iso«-Capability Exklusivrechte besitzt, löscht »push()«»res« zunächst über »consume res«.

Listing 6

main.pony

01 actor Main
02   var env: Env
03
04   new create(env': Env) =>
05     env = env'
06     let picasso: Painter = Painter(this)
07     for x in [Amber, Blue, Crimsen, Crimsen, Other].values() do
08       let res: Ressource iso = recover Ressource(x) end
09       picasso.paint(consume res)
10     end
11
12   be notify(str: String) =>
13     env.out.print(str)

Die nachfolgende Zeile sendet die Ausgabe an die Methode »stats()« aus Zeile 17. Der Code schickt dabei die in »listener« gespeicherte Referenz auf den Actor »Main« an das Behaviour »notify« zurück (Listing 6, Zeile 12).

Doch weiter in Listing 5. Die Methode »stats()« iteriert ab Zeile 19 über alle Farben aus den Primitiven von Listing 4. Die Primitive liest die Schleife aus dem Rückgabewert der ebenfalls aus Listing 4 bekannten Methode »list()«.

Intern arbeiten Schleifen in Pony nicht direkt auf Feldern, sondern auf ihrem Iterator-Objekt. Das erzeugt Listing 5, indem es am Ende ».values()« anhängt (Zeile 21). Für jedes Primitiv iteriert die Laufvariable »x« über alle empfangenen Ressourcen. Zeile 22 summiert die verbrauchte Farbmenge und verwendet dazu die Methode »level()« aus Listing 3. Zum Filtern setzt die Zeile einen »if«-Ausdruck ein. Für alle nicht passenden Primitive aus der Historie steuert der »else«-Zweig eine 0 zur Summe bei.

Auch an anderer Stelle wartet eine Entscheidung. Erweist sich der Farbvorrat von »res« in Zeile 9 als leer, arbeitet das Programm den »else«-Zweig ab Zeile 12 ab. Die Variable »res« erfüllt mit der Reference Capability »iso« die geforderten Schreib- und Leserechte aus der Deklaration der Methode »fill()« mit der Angabe »ref« (Listing 3). Dadurch erhöht der Aufruf in Zeile 13 von Listing 5 das private Feld »_amount« um 1. Dann sendet »paint«»res« rekursiv an sich selbst, löscht aber in Zeile 14 einmal mehr »res« mit Hilfe von »consume«.

Subtyping

Listing 5 weist noch eine weitere Inkompatibilität der Capabilities auf: Denn in der Deklaration von »history« steht »box« (Zeile 3), während die Zeile 10 mit »res« eine »iso«-Referenz über »push()« einzufügen versucht.

Natürlich löscht »consume«»res«. Doch ohne Referenzen von Capatibilies ineinander überführen zu können, ließe sich kein sinnvolles Programm unter Pony schreiben. Abbildung 5 zeigt die möglichen Ersetzungen, die in Pfeilrichtung funktionieren. So lässt sich »iso« für jede andere Capability einsetzen, aber »trn« nur für »val« oder »ref«, weil »trn« noch weitere lesende Referenzen der Capability »box« erlaubt, die dem exklusiven Charakter von »iso« widersprechen würden. »val« lässt sich durch »trn« ersetzen, falls »consume« die letzte noch schreibende Referenz vom Typ »trn« entfernt.

Abbildung 5: In Pfeilrichtung lassen sich Reference Capabilities transitiv ersetzen. Links neben dem Pfeil steht jeweils der Subtyp.

Abbildung 5: In Pfeilrichtung lassen sich Reference Capabilities transitiv ersetzen. Links neben dem Pfeil steht jeweils der Subtyp.

Wer »ref« ersetzt, darf auch weitere schreibende Referenzen erlauben – kein Problem. Die Capabilities »box« und »tag« erweisen sich als unkritisch für »val« und »ref«, da Letztere nicht schreiben können. Einen ausführlicheren Erklärungsversuch unternimmt ein Blogbeitrag von Entwickler John Mumm [6].

Die blauen Pferde

Den Einstiegspunkt in das Beispielprogramm speichert schließlich die Datei »main.pony« aus Listing 6, denn in der Tradition von C verwendet Pony ebenfalls »main« als Bezeichner für den Einstiegspunkt des Programms.

Pony verwendet analog zur bekannten »main()«-Funktion unter C oder der »main()«-Methode unter Java den gleichnamigen Actor, den Listing 6 definiert. Der Konstruktor des Aktors übernimmt in Zeile 4 eine Referenz auf die ausführende Umgebung des Programms in Form eines Objekts vom Typ »Env«. Die folgende Zeile speichert die Referenz in dem Feld mit dem Namen »env« unter der Instanz von »Main« ab.

Zeile 6 erzeugt den Actor vom Typ »Painter«, wie ihn Listing 5 definiert. Der Konstruktor empfängt mit »this« zugleich eine rückbezügliche Referenz auf den »Main«-Actor. Die »for«-Schleife ab Zeile 7 iteriert über das Feld aus den Primitiven »[Amber, Blue, Crimsen, Crimsen, Other]« – oder besser – über dessen Iterator-Objekt.

Zeile 8 generiert anhand der Feldwerte ein Objekt vom Typ »Ressource«, wie in Listing 3 festgelegt ist. Sie speichert die Referenz in der Laufvariablen »x«. Der Ausdruck »recover Ressource(x) end« stellt einen Mechanismus vor, um eine Capability willkürlich zu setzen. In der vorliegenden Form erzeugt »recover« eine Referenz vom Typ »iso«.

Zeile 9 übermittelt die »Ressource« an den Actor »Painter«. Vor dem Versenden steht ein weiterer Löschvorgang mit »consume res« auf dem Programm. Wie schon weiter oben gezeigt, ruft »Painter« schließlich in Zeile 11 von Listing 5 das Behaviour »notify« auf, das Zeile 12 von Listing 6 definiert. Die Nachricht ist vom Typ String und landet dank des »Env«-Objekts in der Shell.

Fazit

Mit Pony schreibt der Anwender offenbar sicheren und hochperformanten Code für nebenläufige Programme. Der ist frei von Data Races und Deadlocks und folgt dem objektorientierten Stil. Das Aktoren-Modell sollte keine Mühe bereiten, doch das schwierige Verständnis der Capabilities dürfte so manchen Anwender abschrecken. Noch komplexer wird die Materie, wenn der Entwickler mehrere Capabilities zusammen nimmt und die resultierenden Effekte betrachtet.

Wer dennoch bei der Stange bleibt, kann die Qualität seiner nebenläufigen Programme steigern. Zudem lässt sich Pony durch Einbinden von C-Code über so genannte FFI an bestehende Projekte andocken.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben