X10 (Programmiersprache)
X10 ist eine parallele, objektorientierte Programmiersprache, für high-end Hardware mit bis zu 10000 Hardware-Threads[1]. Sie wurde 2004 bei IBM am Forschungszentrum Thomas J. Watson als Teil des PERCS-Projekts entwickelt. Zielplattformen der Programmiersprache sind Cluster-Systeme mit unterschiedlichen Berechnungseinheiten (Non-Uniform Cluster Computing). Die Programmiererproduktivität solcher Systeme soll mit X10 um den Faktor zehn erhöht werden, was zu dem Namen X10 geführt hat.[2] Die Entwicklung von X10 wurde durch das High Productivity Computing Systems (HPCS) Programm der DARPA finanziert.
X10 | |
---|---|
Paradigmen: | Objektorientierte Programmiersprache |
Erscheinungsjahr: | 2004 |
Designer: | Kemal Ebcioglu, Vivek Sarkar, Vijay Saraswat |
Entwickler: | Kemal Ebcioğlu, Vijay Saraswat und Vivek Sarkar (IBM) |
Aktuelle Version: | 2.6.2[1] (8. Januar 2019) |
Typisierung: | stark, statisch |
Beeinflusst von: | Java, C++, Scala |
Betriebssystem: | AIX, Linux, macOS, Windows/Cygwin |
Lizenz: | Eclipse Public License 1.0 |
http://x10-lang.org/ |
X10 wurde speziell zur parallelen Programmierung nach dem Partitioned Global Address Space (PGAS) Modell entworfen. X10 erweitert dieses sogar noch um Asynchronität, was zu einem APGAS Modell führt. Eine Berechnung ist auf verschiedene Places aufgeteilt. Diese enthalten Daten und eine oder mehrere Activities, die mit diesen Daten arbeiten. X10 hat ein beschränktes Typsystem für objektorientierte Programmierung. Außerdem hat es noch andere Eigenschaften wie benutzerdefinierte primitive struct-Typen, global verteilte Arrays und strukturierten und unstrukturierten Parallelismus.[1]
Grundkonzepte
Activities
Eine Activity ist ein leichtgewichtiger Thread ohne eigenen Namen. Asynchrone Activities werden durch den Befehl
at (p) async s
erstellt. Dabei ist p der Place, an dem die Activity ausgeführt werden soll und s der Befehl (engl. Statement). Eine Aktivität wird an einem bestimmten Place erstellt und bleibt dort für die gesamte Lebensdauer. Mit dem Schlüsselwort here kann eine Aktivität auf ihren Place zugreifen.[3]
Places
Ein Place enthält Daten und Activities, die auf diesen Daten arbeiten. Man kann sich einen Place als eigenen Knoten einer verteilten Java Virtual Machine (JVM) mit eigenem Heap und eigenen Threads vorstellen. Dabei kann angenommen werden, dass alle Places geordnet sind. Handelt es sich bei p um einen Place, so kann mit p.next auf den nächsten Place zugegriffen werden.[3]
Es gibt keinen expliziten Befehl um einen neuen Place zu erzeugen, vielmehr wird jede Activity mit einer expliziten Anzahl Places gestartet. Daraus ergibt sich eine fixe Anzahl von Places für das gesamte Programm, die während der gesamten Programmlaufzeit konstant bleibt. Die Places des Programms sind von 0 bis Place.MAX_PLACES-1 nummeriert und werden in einer sortierten Sequenz von Place.places() gespeichert. Auf den Place der aktuellen Activity kann mit dem Schlüsselwort here zugegriffen werden.[1]
Der Zugriff abhängig vom Place wird durch das statische Feld location, das jedes Objekt enthält, geregelt. Nur die Objekte eines Places erhalten Zugriff auf nicht-finale Felder der anderen Objekte dieses Places. Versuche auf nicht-finale Felder von Objekten auf einem anderen Place zuzugreifen scheitern mit einer BadPlaceException (BPE).[3] Man kann jedoch mit dem Ausdruck at einzelne Activitys synchron auf andere Places verschieben. Dabei handelt es sich, wie bei jeder verteilten Operation, um eine sehr teure Operation. Sie bildet jedoch die Grundlage für Multicore Programmierung in X10.[1]
Objekte werden auf dem Place erzeugt, auf dem der Konstruktor-Aufruf läuft. Anschließend kann ein Objekt, im Gegensatz zu Activities, nicht mehr den Place wechseln. Es kann nur auf einen anderen Place kopiert oder über GlobalRef von anderen Places referenziert werden.[1]
Distributions und distributed Arrays
Mit Distributions werden distributed Arrays auf Places verteilt. Dabei ist die Distribution ein Objekt, die jedem Element des Arrays einen Place zuordnet. Auf welchem Place ein Element eines distributed Arrays liegen soll, muss bereits zur Array-Erstellung bekannt sein. Dabei ist zu beachten, dass auf einem Place nur auf diejenigen Elemente des distributed Arrays zugegriffen werden kann, die auch auf diesem Place liegen.[3]
Atomarität
Wie in vielen anderen Programmiersprachen verbergen sich hinter einfachen Befehlen, wie dem Inkrement mehrere Maschinenbefehle, was diese zu einem kritischen Abschnitt macht. Das bedeutet, dass ein solcher Befehl potentiell von einem parallel ausgeführten anderen Befehl unterbrochen werden kann. Um das zu verhindern können in X10 einzelne Befehle mit einem
when(c) S
umgeben werden. Dabei ist c die boolesche Wächterbedingung und S der atomar auszuführende Befehl (engl. statement). Solange c nicht true ist, blockiert die Ausführung. Wenn c true wird, so wird S atomar ausgeführt.[3]
Um ganze Methoden atomar auszuführen, kann man das Schlüsselwort atomic davor schreiben. Die Methode increment wird beispielsweise atomar ausgeführt[3]:
atomic boolean increment() {
value++;
return true;
}
Clocks
Viele parallele Algorithmen sind in verschiedene Phasen aufgeteilt, dabei müssen alle Aktivitäten einer Phase abgearbeitet werden, bevor die nächste Phase beginnt. In X10 werden diese Phasen durch sogenannte Clocks getrennt. Um eine Aktivität an einen solchen Clock zu binden, definiert man diese als clocked() mit dem entsprechenden Clock Objekt.[1]
Im folgenden Beispiel werden zwei Aktivitäten A und B durch ein Clock Objekt synchronisiert, so dass zuerst die Ausgaben der ersten Phase beider Aktivitäten ausgegeben werden und anschließend die Ausgaben der zweiten Phase beider Aktivitäten[Anmerkung 1]:
//erzeuge ein Clock Objekt
val clock = Clock.make();
//erzeuge Aktivität A und binde sie an clock
async clocked(clock) {
//gebe parallel zu B Text aus
print("A: 1st phase. ");
//warte auf B
Clock.advanceAll();
print("A: 2nd phase. ");
}
//erzeuge Aktivität B und binde sie an clock
async clocked(clock) {
//gebe parallel zu A Text aus
print("B: 1st phase. ");
//warte auf A
Clock.advanceAll();
print("B: 2nd phase. ");
}
Ein mögliches Ergebnis des Beispiels wäre:
A: 1st phase. B: 1st phase. B: 2nd phase. A: 2nd phase. ,
nicht aber
A: 1st phase. A: 2nd phase. B: 1st phase. B: 2nd phase. .
Klassen
Eine Klasse enthält, analog zu Java, Daten und Code. Eine Klasse kann verschiedene Felder, Eigenschaften, Methoden, Konstruktoren enthalten. Außerdem spezifiziert eine Klasse maximal eine oder keine Elternklasse. Eine Klasse kann mehrere Interfaces implementieren.
Eine Klasse kann als final markiert werden. Von einer solchen Klasse kann nicht mehr geerbt werden.
Felder
Felder spezifizieren Daten, die zu einer Klasse gehören. Dabei unterscheidet man, ob die Felder veränderbar (gekennzeichnet mit dem Schlüsselwort var), oder unveränderbar (gekennzeichnet mit dem Schlüsselwort val) sind. Der Typ eines veränderbaren Feldes muss immer spezifiziert werden. Dieser folgt dem Namen der Variable nach einem Doppelpunkt. Unveränderbare Felder können bereits mit einem Wert initialisiert werden, müssen es aber nicht.
//veränderbares Feld mit dem Namen "population", vom Typ Integer und mit dem initialen Wert 42
var population:Int = 42;
//veränderbares Feld mit dem Namen "growthRate", vom Typ Integer und ohne initialen Wert
var growthRate:Int;
Der Typ eines unveränderbaren Feldes muss nicht explizit angegeben werden. Er kann auch implizit aus dem Initialisierungswert abgeleitet werden. Ein unveränderbares Feld muss nicht initialisiert werden, solange es im Konstruktor der Klasse initialisiert wird.
//unveränderbares Feld mit dem Namen "pi", vom expliziten Typ Double und ohne initialen Wert
val pi:Double;
//unveränderbares Feld mit dem Namen "truth", vom impliziten Typ Integer und mit initialem Wert 42
val truth = 42;
Felder können entweder objektspezifisch oder statisch (mit dem Schlüsselwort static gekennzeichnet) sein. Standardmäßig sind Felder objektspezifisch, das heißt jedes Objekt hat seine eigene Instanz des Feldes. Ist ein Feld statisch, so gibt es für alle Objekte nur eine Instanz dieses Feldes. In X10 müssen statische Variablen immer unveränderbar sein.
//statisches, unveränderbares Feld mit dem Namen "pi", vom impliziten Typ Double und mit initialem Wert
static val pi = 3,141;
//objektspezifisches, unveränderbares Feld mit dem Namen "truth", vom impliziten Typ Integer und mit initialem Wert
val truth = 42;
Implementierungen
Es gibt zwei Implementierungen von X10: Native X10 und Managed X10. Native X10 basiert auf C++ und bildet einen Place auf einen Prozess ab. Es ist für folgende Betriebssysteme verfügbar: Linux, AIX, Mac OS, Cygwin und Blue Gene. Managed X10 ist JVM basiert. Es bildet einen Place auf einen JVM Prozess ab. Da es auf Java basiert, läuft es unabhängig vom Betriebssystem mit jeder Version ab Java 6.[4]
Einzelnachweise
- Vijay Saraswat, Bard Bloom, Igor Peshansky, Olivier Tardieu, David Grove,: X10 Language Specification Version 2.6.2. 2019, S. 1–303 (sourceforge.net [PDF; 1,5 MB; abgerufen am 4. Januar 2019]).
- Philippe Charles, Christian Grothoff, Vijay A. Saraswat, et al.: X10: An Object-Oriented Approach to Non-Uniform Cluster Computing. In: OOPSLA. 2005, S. 519–538, doi:10.1145/1094811.1094852.
- Vijay Saraswat, Radha Jagadeesan: Concurrent Clustered Programming. In: Lecture Notes in Computer Science. Band 3653. Springer, Berlin Heidelberg 2005, S. 353–367, doi:10.1007/11539452_28.
- David Grove, Vijay Saraswat, Olivier Tardieu: Developing Scalable Parallel Applications in X10. 2012, S. 1–85 (online [PDF; 1,3 MB; abgerufen am 26. August 2013]).
Anmerkungen
- Aktivitäten haben in X10 keine Namen, im Beispiel werden die Aktivitäten nur aus didaktischen Gründen benannt.