Dart Isolates

Dart ist eine noch sehr junge, aber auch vielversprechende Sprache. Man kann es wie JavaScript aussehen lassne, aber auch wie Java. Einer der Unterschiede zu JavaScript ist dessen fehlende Multithreading Unterstützung. Lediglich der Chrome Browser kann mit einiger schwarzen Magie mehrere Threads ausführen – aber selbstverständlich sollte man sich darauf nicht verlassen.

Dart behebt diesen Umstand, denn mit Dart ist eine Art von Multithreading möglich. Was Java also mit Threads kann, kann Dart mit den sogenannten “Isolates”. Isolates sind ziemlich cool. Die Idee stammt von Erlang und tatsächlich gibt es eine Art Zusammenarbeit zwischen den Entwicklern von Erlang und den Entwickler von Dart.

Um Isolates zu erstellen, muss man folgende Klassen kennen:

Isolates sind vollkommen unabhängig von einander. Das bedeutet, sie teilen sich keine gemeinsamen Variablen – weder statische- noch Instanzvariablen. Isolates können untereinander nur via so genannten Ports kommunizieren. Das hat einen Vorteil: Teile des Dart-Programms können neu gestartet werden, wenn etwas schief läuft. Aber Achtung: es gibt zwei Geschmacksrichtungen von Isolates, die ich später noch erkläre.

Ein Isolate is so lange verfügbar, so lange auch sein Port offen ist. Wird der Port geschlossen, verschwindet auch der Isolate. Derzeit gibt es keine Möglichkeit innerhalb Darts zu prüfen, ob der Isolate noch vorhanden ist oder nicht, der Entwickler verlässt sich hier auf die Dart VM.

Aber sehen wir uns nun Isolates im Detail an. Unser Plan ist es, eine Klasse “Starter” zu erzeugen, die eine andere Klasse “Worker”.

Die Worker Klasse – Isolate erweitern

Die Worker Klasse ist tatsächlich erstmal ziemlich einfach. Es sieht folgendermaßen aus:

class Worker extends Isolate {
	Worker() : super.heavy();

	main() {
		this.port.receive(
        	void _(var message, SendPort replyTo) {
        		print ("Worker receives: ${message}");
        		replyTo.send("Pong");
				this.port.close();
	        }
	    );
	}
}

Wie man sehen kann, leitet die Worker Klasse von Isolate ab. In der nächsten Zeile wird der super-Konstruktor aufgerufen. Tatsächlich handelt es sich hierbei um einen “named” Konstruktor mit dem Namen “heavy”. Wie bereits erwähnt werde ich später noch auf den Unterschied zwischen “light” und “heavy” eingehen.

Sobald ein Isolate startet (der Begriff ist: “spawned”, in der Starter-Klasse werden wir dies noch tun), wird das von Isolate geerbte Feld “port” mit einem ReceivePort belegt. Damit kann der Isolate Nachrichten von einem anderen Isolate empfangen.

Aus diesem Grund ist es auch wichtig, eine Funktion zur “receive()” Methode hinzuzufügen. Ansonsten wird nur die main() Methode einmal durchlaufen und das wars. Im schlimmsten Fall vergisst der Entwickler den this.port zu schließen, und dann bleibt der Isolate so lange “am Leben”, so lange auch die VM läuft. Ehrlich gesagt, die VM stoppt auch nicht, so lange ein Isolate läuft (es sei denn man verwendet Brute Force).

Daher ist man gut beraten, wenn man nur notwendigen Intialisierungscode in der main() Methode ausführt und dann eine Methode implementiert, die die tatsächliche Arbeit erledigt, und zwar immer dann, wenn eine Nachricht empfangen wurde. Zudem sollte diese Methode sich darum kümmern, dass gegebenenfalls der Port geschlossen wird.

Genauso etwas habe ich in der Worker Klasse gemacht. Hier sieht man die main() Methode und außerdem wie ich receive() mit einer anonymen Funktion aufrufe. Ab jetzt wird dann meine Funktion aufgerufen, wenn eine Nachricht ankommt.

In meiner anonymen Funktion kann man den “message” Parameter sehen, der so gut wie alles sein kann – der ist nämlich vom Typ dynamic. Der zweite Parameter ist ein SendPort. Damit erhält mein Isolate die Chance, dem ursprünglichen Sender der Nachricht zu antworten. Ich tue das auch, und sende “Pong” zurück.

Das war jetzt ziemlich einfach. Sehen wir uns die nächste Klasse an, die den “Worker” spawned.

Die Starter Klasse

Hier ist der Code:

class Starter {
	ReceivePort _receivePort;

	Starter.start() : 
		_receivePort = new ReceivePort() {
			this._receivePort.receive(
				void _(var message, SendPort replyTo) {
	        		print ("Receiving from Worker: ${message}");
	        		_receivePort.close();
		        }
			);

			Worker worker = new Worker();
			worker.spawn().then((SendPort port) {
				port.send('Ping', _receivePort.toSendPort());
		});		
	}
}

OK, das sieht erstmal etwas komplizierter aus, ist es aber nicht. Ich habe eine normale Klasse mit dem Namen “Starter” erstellt und das Feld _receivePort definiert. Immerhin möchte ich ja auch Antworten von dem Isolate erhalten, dass ich erstelle.

Als nächstes erstelle ich den Konstruktor “start()” und initialisiere meinen ReceivePort in Zeile 5. Ähnlich wie in meiner “Worker” Klasse erstelle ich eine Funktion in meinem ReceivePort, die immer dann aufgerufen wird, wenn ich eine Antwort an diesem Port erhalte. Tatsächlich wird hier die “Pong”-Nachricht aus dem Worker ankommen.

In Zeile 13 erstelle ich ein neues “Worker” Objekt. Hier passiert erstmal noch gar nichts besonderes, denn der “Worker” wird erst dann zu einem echten Isolate, wenn die spawn() Methode in Zeile 14 aufgerufen wird. Nachdem spawn() meinen Isolate erstellt hat, wird then() aufgerufen und bekommt einen SendPort als Parameter. Überraschung: denn der SendPort ist eben jener Port, der von Dart in dem Worker Feld “port” abgelegt wurde.

Nun kann man den SendPort verwenden um eine Nachricht zu erstellen. Das tue ich in Zeile 15: dort wird die Nachricht versendet und außerdem noch ein SendPort an den Worker mitgeliefert. Glücklicherweise verfügt die ReiverPort Klasse über die toSendPort() Methode, die eben einen solchen Rückkanal erzeugt.

Fertig – oh einen Moment noch. Starten wir die ganze Sache doch mal:

main() {
	new Starter.start();
}

Jetzt kann man das Ergebnis beobachten: Das Programm sollte nachdem die beiden Nachrichten versendet wurden beendet werden. Falls dies nicht geschieht, sind möglicherweise noch Ports offen.

Der Unterschied zwischen light und heavy Isolates

Falls man einen Isolate erstellen möchte, hat man die Wahl zwischen “light” und “heavy” Isolates. Die erste Geschmacksrichtung bekommt man per Default. Aber was ist nun der Unterschied? Tatsächlich starten beiden Isolate-Arten mit einem default Zustand und können auch nur über Ports kommunizieren. Und selbstverständlich sind auch beide Sorten asynchron.

Die Erklärung ist einfach: “light” Isolates werden im gleichen Thread ausgeführt, wie der Isolate, der sie erstellt hat. Man könnte sagen, die Sache verhält sich so wie in JavaScript. Nur eine Ausführung kann gleichzeitig geschehen. Falls eine Nachricht ankommt, wird die erstmal in einer Queue gepuffert, bis der gegenwärtig laufende Isolate beendet wurde und eben dann die nächste Nachricht von der Queue genommen werden kann.

“Heavy” Isolates auf der anderen Seite laufen als echter Thread in der VM. Und der kann natürlich parallel zum Erzeuger seine Arbeit machen. Auch heavy Isolates bekommen Nachrichten in ihre Queue, und diese werden bearbeitet, sobald Sie Ihre Isolate Loop beendet haben. EIn Isolate läuft also immer single threaded, und das heißt, man muss sich nicht um Thread Safety kümmern. Das ist natürlich ein besonderes Schmankerl.

Auf der anderen Seite muss man natürlich mehrere Isolates erzeugen, wenn man mal etwas mehr Power benötigt. Spawning benötigt aber auch etwas Performance. Daher macht es vielleicht Sinn für so einige Anwendungen, dass Sie eine Art Isolate Pool erzeugen, sobald die VM startet.

Anmerkungen

Dart ist natürlich noch eine junge Sprache, und daher wird es Änderungen geben. Dies gilt speziell für Isolates. Im Moment gibt es die Unterscheidung von light und heavy nur für DartC, und das auch nur, wenn Webworker im Browser verfügbar sind.

Auf der VM Seite wird jeder neue Isolate als neuer, nativer Thread gestartet. Es gibt also keine Unterscheidung zwischen “light” und “heavy” – noch.

Wie bereits erwähnt gibt es noch einigen Diskussionsbedarf über Isolates. Es kann passieren, dass die Begriffe light/heavy komplett von der Bildfläche verschwinden und dafür etwas vollkommen neues auf der Dart-Bühne erscheint. Sogar die Idee für einen Threadpool für “light” Isolates wird gerade diskutiert.

Tags: ,

4 Responses to “Dart Isolates”

  1. Dart erreicht Chromium - und begegnet Scala | Dart Vader Says:

    [...] Framework arbeitet auf Basis der Darts Isolates – eine Möglichkeit der parallelen Verarbeitung in Dart. Wenn man sich, das so ansieht, wird [...]

  2. 10 Punkte, in denen Dart JavaScript übertrifft | Dart Vader Says:

    [...] Dart kennt Isolates. Das ist damit so ähnlich wie in Erlang. Isolates können miteinander kommunizieren. Wenn ein Isolate zusammenbricht, kann es neu gestartet werden. Das ist ziemlich cool. Natürlich machen Isolates Dart auch für die serverseitige Entwicklung interessant. Und ja, ich habe von Node.js gehört. Aber das ist nicht der Punkt: Dart kann das nämlich von sich aus. [...]

  3. Dart: 10 Punkte, in denen es JavaScript übertrifft » t3n News Says:

    [...] Dart kennt Isolates. Das ist damit ähnlich wie in Erlang. Isolates können miteinander kommunizieren. Wenn ein Isolate zusammenbricht, kann es neu gestartet werden. Das ist ziemlich cool. Natürlich machen Isolates Dart auch für die serverseitige Entwicklung interessant. Und ja, ich habe von Node.js gehört. Aber das ist nicht der Punkt: Dart kann das nämlich von sich aus. [...]

  4. Dats neue Isolates | Dart Vader Says:

    [...] Dart sind die Isolates (eine Art Multithreading Feature für Dart, falls Sie es noch nicht kennen. Jedoch war die erste Version der API noch sehr komplex. Sie erinnerte sehr stark an das Java Threading und war generell sehr schwer zu erlernen. Die [...]

Leave a Reply