Die Lösung für nebenläufiges Denken: Promises

Design Patterns, Erklärungen zu Algorithmen, Optimierung, Softwarearchitektur
Forumsregeln
Wenn das Problem mit einer Programmiersprache direkt zusammenhängt, bitte HIER posten.
Antworten
antisteo
Establishment
Beiträge: 854
Registriert: 15.10.2010, 09:26
Wohnort: Dresdem

Die Lösung für nebenläufiges Denken: Promises

Beitrag von antisteo »

Ich bin seit Tagen begeistert von den Promises, die JS anbietet, da es das Problem immer komplexer werdender asynchroner Verschachtelungen (auch Callback Hell) für mich löst. Da dachte ich, ich teile es gleich mal mit dem Forum, um evtl. eine Diskussion anzustacheln über Für- und Wider von Asynchronem Gedenke und wie man das Wirrwar an nebenläufigen Dingen unter Kontrolle bekommt.

Zuerst mal meine (momentan) favorisierte Bibliothek für Promises, dokumentiert hier: http://bluebirdjs.com/docs/api-reference.html

Promises sind quasi Futures von hinten aufgezäumt. Ein Promise ist ein Future, der entweder einen Erfolg inklusive Rückgabewert oder einen Misserfolg inklusive Fehler-Objekt (meist ein Stacktrace) liefert. Rufe ich also einen HTTP-GET-Request aus, kann dieser mir entweder das Dokument liefern oder einen Fehler.

Das Coole daran sind jetzt die Features, die ich damit habe:
- thenable ist das wichtigste Feature eines Promises. Ein Promise ist ein Promise, wenn es die then()-Funktion aufweist. In der then()-Funktion übergibt man 2 Callbacks: Success und Failure.
- Ruft man then auf, erhält man wiederum einen Future
- Ich kann in einem then-Callback Werte oder weitere Futures zurückgeben. Gebe ich einen Wert zurück, wird dieser als Erfolg für den then-Future in die nächste Kette gefüttert. Gebe ich einen Future zurück, wird dieser als nächstes ausgeführt.
- Ich kann auf ein Array von Futures warten und bekomme ein Array von Werten
- Ich kann beliebig eigene Promises kreieren und muss lediglich dafür sorgen, dass entweder resolve() oder reject() aufgerufen wird.
- Durch das Verketten von Promises (.then() returnt einen Anschluss-Promise) wird aus 100 Verschachtelungen auf einmal 100 verkettete .then()s
- So langsam setzt sich durch, dass beliebige APIs auf einmal statt normalen Werten auch Promises entgegennehmen. Man muss Werte also gar nicht mehr auswerten, sondern übergibt lediglich Futures darauf.
- Es gibt so eine hübsche Funktion .promisifyAll, die wandelt die komplette node.js-API in Promise-enabled Funktionen um.
- Mit JS-Coroutinen kann man auf einmal sequenziellen Code schreiben, der asynchron ausgeführt wird. Das sieht dann so aus:

Code: Alles auswählen

var myFunction = Promise.coroutine(function* (keyword) {
    wikipediaPage = yield $http.get('https://de.wikipedia.org/wiki/' + keyword);
    // tue irgendwas mit wikipediaPage
});

myFunction('JavaScript');
Der Rückgabewert der neuen Funktion ist natürlich ebenfalls ein Promise. Mit yield hält man die Coroutine an und wartet auf das Ergebnis eines Promises. Will man dann Rejections der Promise abfangen, schreibt man einen try-catch-Block drumherum.

Ich wollte eigentlich mal eine Programmiersprache entwickeln, die genau so mit Futures umgeht, aber jetzt kann es auf einmal JavaScript frei haus.
http://fedoraproject.org/ <-- freies Betriebssystem
http://launix.de <-- kompetente Firma
In allen Posts ist das imo und das afaik inbegriffen.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Ich glaube in Java gibt's dafür jetzt das sicher weniger schön aussehende CompletionStage-Konstrukt (https://docs.oracle.com/javase/8/docs/a ... Stage.html).

Was ich mich bei sowas immer Frage, macht das wirklich Sinn Aufrufe derartig zu verketten (mittels thenable)? Ich dachte node.js macht gerade Spaß, weil man eben nicht nebenläufig arbeiten muss. Wäre es nicht besser verständlich und einfacher so zu arbeiten?

myPromise1 = ...
myPromise2 = ...
myPromise3 = ...
doThing1(myPromise1.get())
doThing2(myPromise2.get(),myPromise3.get())

Bei Exceptions sollte ja das throw early catch late Prinzip gelten, so dass es i.d.R. doch ausreichen sollte um alles einen try-catch-Block zu basteln.
antisteo
Establishment
Beiträge: 854
Registriert: 15.10.2010, 09:26
Wohnort: Dresdem

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von antisteo »

Chromanoid hat geschrieben:Ich glaube in Java gibt's dafür jetzt das sicher weniger schön aussehende CompletionStage-Konstrukt (https://docs.oracle.com/javase/8/docs/a ... Stage.html).

Was ich mich bei sowas immer Frage, macht das wirklich Sinn Aufrufe derartig zu verketten (mittels thenable)? Ich dachte node.js macht gerade Spaß, weil man eben nicht nebenläufig arbeiten muss. Wäre es nicht besser verständlich und einfacher so zu arbeiten?

myPromise1 = ...
myPromise2 = ...
myPromise3 = ...
doThing1(myPromise1.get())
doThing2(myPromise2.get(),myPromise3.get())

Bei Exceptions sollte ja das throw early catch late Prinzip gelten, so dass es i.d.R. doch ausreichen sollte um alles einen try-catch-Block zu basteln.
Die IO-Nebenläufigkeit passiert in node.js meist über:
- Mehrere Nutzer, die das System anfragen
- Mehrere Ressourcen, die du anfragst (wie in deinem Beispiel)

Mit den Promises hast du lediglich eine schönere Schreibweise, Daten- und Kontrollflussabhängigkeiten aufzureihen, anstatt in verschachtelten Callbacks zu sterben. Den Code in deinem Beispiel könnte man auf 2 Weisen schreiben:

Coroutine-Variante

Code: Alles auswählen

Promise.coroutine(function*() {
myPromise1 = ...
myPromise2 = ...
myPromise3 = ...
doThing1(yield myPromise1);
doThing2(yield myPromise2, yield myPromise3);
});
oder unter Verwendung von http://bluebirdjs.com/docs/api/promise.all.html

Code: Alles auswählen

myPromise1 = ...
myPromise2 = ...
myPromise3 = ...
Promise.all([myPromise1, myPromise2, myPromise3]).then(function (result) {
  doThing1(result[0]);
  doThing2(result[1], result[2]);
});
http://fedoraproject.org/ <-- freies Betriebssystem
http://launix.de <-- kompetente Firma
In allen Posts ist das imo und das afaik inbegriffen.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Also ich hab ja mein Beispiel mit Java Futures geschrieben. Da ist ein get() ja praktisch ein yield (https://docs.oracle.com/javase/7/docs/a ... e.html#get()).

Während Dein erstes Beispiel "Coroutine-Variante" noch äquivalent ist, ist das zweite Beispiel nicht äquivalent. Es müsste doch so heißen:

Code: Alles auswählen

myPromise1 = ...
myPromise2 = ...
myPromise3 = ...
doThing1(yield myPromise1);
yield Promise.all([myPromise2, myPromise3]).then(function (result) {
  doThing2(result[0], result[1]);
});
oder und jetzt kommt das was ich mit verschachteln meinte:

Code: Alles auswählen

myPromise1 = ...
myPromise2 = ...
myPromise3 = ...
yield myPromise1.then(function (result1) {
  doThing1(result1);
  yield Promise.all([myPromise2, myPromise3]).then(function (result2) {
    doThing2(result2[0], result2[1]);
  });
}
Bei den then Dingern, ist die Fehlerbehandlung auch eher unspannend, da man ja irgendwie die Fäden wieder zusammenführen muss, oder?

Das then-Zeug taugt dann eher für fire-und-forget-Sachen oder?

Wenn ich das richtig verstehe, ist die "Einführung" von yield für Promises das eigentlich coole in node.js oder? (also weil es das vorher nicht gab und daher andere Sprachen wie C# oder Java Callback-Hells umgehen können, während man in node.js bis yield keine andere Möglichkeit hatte...)
http://wingolog.org/archives/2013/05/08 ... tors-in-v8
http://blog.alexmaccaw.com/how-yield-wi ... sform-node
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Danke euch beiden für das Thema und die diversen Links. Ich habe viel gelernt.

Unzufrieden bin ich eigentlich nur mit dem Threadtitel.

Erstens würde ich "Nebenläufigkeit" bzw. "nebenläufig" mit "Concurrency" assoziieren und nicht mit "Asynchronous" as in "AJAX". Die Beispiele sind aber alle für "Asynchronous", was mich zunächst ziemlich verwirrt hat. Es ist alles andere als Konsens, dass das überhaupt ein Unterschied ist, aber ich denke es macht Sinn, da zu differenzieren. Andere Menschen die das tun findet man hier:
http://blog.stevenedouard.com/asynchron ... ncurrency/
Und natürlich bei Stackoverflow.

Mit dem Hintergrund kann man leicht Chromanoids Einwände beantworten:

"Ich dachte node.js macht gerade Spaß, weil man eben nicht nebenläufig arbeiten muss." -> Stimmt, von Nebenläufigkeit ist auch nur im Titel die Rede. Gemeint ist Asynchronizität / "Non-blocking I/O"

"Das then-Zeug taugt dann eher für fire-und-forget-Sachen oder?"-> Stimmt auch. Ansynchronizität ist nur ein schöner Name für "fire and forget Nebenläufigkeit".

Bzw. sorry, das Begriffswirrwar ist fürchterlich. Die richtige Formulierung ist wahrscheinlich, dass Asynchronizität, auch wenn sie nicht nebenläufig implementiert ist, wie scheints in node.js, von außen wie Nebenläufigkeit aussehen kann. Genauso wie die alten Windowse auf single-core Maschinen schon "Multitasking" konnten. Möglicherweise hat das den Threadersteller auch zu seiner Titelwahl hingerissen.

Zweitens, um wieder auf den Titel zu kommen, wird hier ja eben gerade keine irgendwie neue Lösung für Nebenläufigkeit präsentiert, sondern eine sehr JavaScript spezifische Lösung für ein sehr JavaScript spezifisches Syntaxproblem. Ich will nicht mit dem Totschlagargument "Syntactic Sugar" kommen, weil "Syntactic Sugar" oft das einzige ist was wir haben, aber im neutralen Sinne des Wortes ist es natürlich genau das.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Mmh, ich kannte bisher nur den Unterschied zwischen Parallelität und Nebenläufigkeit:
Parallelität -> Zwei Ereignisse finden parallel statt, wenn sie gleichzeitig ausgeführt werden.
Nebenläufigkeit -> Zwei Ereignisse sind nebenläufig, wenn sie parallel ausgeführt werden können (jedoch nicht müssen), das heißt, wenn zwischen ihnen keine kausale Abhängigkeit besteht.
Nebenläufigkeit ist der allgemeinere Begriff.

Für mich ist non-blocking IO nebenläufig, weil der Datenempfangsprozess nebenläufig/asynchron zum Verarbeitungsprozess arbeitet. Die Definition von Non-blocking IO ist doch im Grunde genau das. Was würde Asynchronität nun von Nebenläufigkeit unterscheiden? Das Waschmaschinenbeispiel bezieht sich glaube ich eher auf Parallelität und nicht auf Nebenläufigkeit, oder? Der Trick bei Node.js ist ja, dass man nebenläufige Ereignisse nicht nebenläufig verarbeitet. Also einen einzigen Verarbeitungsprozess für eine serialisierte Kette von nebenläufigen Ereignissen verwendet.

Ich habe nie mit node.js gearbeitet und daher immer angenommen, dass das was man für node.js implementiert, immer wie die yield-Beispiele aussieht (also höchst sequentiell und imperativ), stattdessen wird i.d.R. mit Callbacks gearbeitet und das ganze fühlt sich an, wie auf Mausklicks in einer Benutzeroberfläche zu reagieren. Das finde ich eher unschön, wenn das Motto ja eigentlich lautet die Probleme nebenläufiger Verarbeitung zu verbergen.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Nachtrag: Was ich sehr spannend an dem yield in node.js finde, ist dass der globale Programmzustand nach einem yield - so wie ich das verstehe - ggf. nicht mehr der ist, der vor dem yield vorlag. D.h. Programme mit yield laufen nicht mehr "atomar und isoliert", sondern nur die einzelnen Teilstücke zwischen den yields, werden atomar und isoliert ausgeführt. An dieser Stelle finde ich die Callbacks wesentlich weniger irreführend. Das yield funktioniert an der Stelle ja praktisch wie ein yield bei einem echten Thread...
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Chromanoid hat geschrieben:Mmh, ich kannte bisher nur den Unterschied zwischen Parallelität und Nebenläufigkeit:
Parallelität -> Zwei Ereignisse finden parallel statt, wenn sie gleichzeitig ausgeführt werden.
Nebenläufigkeit -> Zwei Ereignisse sind nebenläufig, wenn sie parallel ausgeführt werden können (jedoch nicht müssen), das heißt, wenn zwischen ihnen keine kausale Abhängigkeit besteht.
Nebenläufigkeit ist der allgemeinere Begriff.

Für mich ist non-blocking IO nebenläufig, weil der Datenempfangsprozess nebenläufig/asynchron zum Verarbeitungsprozess arbeitet. Die Definition von Non-blocking IO ist doch im Grunde genau das.
Ich führe das jetzt einfach mal darauf zurück, dass Englisch hier die kanonische Sprache ist uns offenbar nicht jeder Nebenläufigkeit so übersetzt wie mein Prof. damals. Ich muss gestehen, seit der Vorlesung habe ich den Begriff auf deutsch bis zu diesem Thread weder gelesen noch verwendet. Mein Punkt waren aber letztlich nicht sosehr die Semantik der konkreten Begriffe sondern die Idee, dass Nebenläufigkeit "all Fun and Games" ist, bis man mal gezwungen ist etwas zu synchronisieren. Im Grunde sagst du es ja selbst. Die hier vorgeschlagene Lösung taugt in der Form nur, solange man die Fäden nicht wieder zusammenführen muss. Fire and Forget, wie du schon sagtest. Für die Probleme, die ich mit Nebenläufigkeit assoziiere ist das eben keine Lösung. Was an sich natürlich total subjektiv ist, wer mit dem Begriff anderes verbindet, wird das anders sehen.
Was würde Asynchronität nun von Nebenläufigkeit unterscheiden? Das Waschmaschinenbeispiel bezieht sich glaube ich eher auf Parallelität und nicht auf Nebenläufigkeit, oder? Der Trick bei Node.js ist ja, dass man nebenläufige Ereignisse nicht nebenläufig verarbeitet. Also einen einzigen Verarbeitungsprozess für eine serialisierte Kette von nebenläufigen Ereignissen verwendet.
Für mich war der eye-opener, zu verstehen, dass sowohl die Quellen zu dem Thema als auch scheinbar der Threadersteller mit Asynchronität eben das "A" in AJAX verbinden, also letztlich die Kommunikation zwischen Client und Server über ein zustandsloses Protokoll (HTTP). Was, da sind wir uns sicher einig, ein sehr spezieller Spezialfall von Nebenläufigkeit ist, aber aus Sicht eines JavaScript Anwenders die Brot und Butter Anwendung. Da prallen Mindsets aufeinander.
Ich habe nie mit node.js gearbeitet und daher immer angenommen, dass das was man für node.js implementiert, immer wie die yield-Beispiele aussieht (also höchst sequentiell und imperativ), stattdessen wird i.d.R. mit Callbacks gearbeitet und das ganze fühlt sich an, wie auf Mausklicks in einer Benutzeroberfläche zu reagieren. Das finde ich eher unschön, wenn das Motto ja eigentlich lautet die Probleme nebenläufiger Verarbeitung zu verbergen.
Ich will mich nicht in Halbwissen versteigen, wie das implementiert ist, aber ich glaube man kommt mit dem simpelst Modell in dem Client und Server in jeweils einer Endlosschleife auf Sockets lauschen schon sehr sehr weit beim Verständnis dessen was da passiert. Das ist deren Nebenläufigkeit. Der Rest, ist Syntax, egal ob man es jetzt als Callback oder yield schreibt. Beide Varianten kaschieren was wirklich passiert. Denn die vom Client registrierte Callback wird ja nicht wirklich im Prozess des Servers aufgerufen und vice versa. Es würde auch auf keiner Ebene Sinn machen, wenn der Server ausführbaren Code über HTTP akzeptieren würde.
Nachtrag: Was ich sehr spannend an dem yield in node.js finde, ist dass der globale Programmzustand nach einem yield - so wie ich das verstehe - ggf. nicht mehr der ist, der vor dem yield vorlag. D.h. Programme mit yield laufen nicht mehr "atomar und isoliert", sondern nur die einzelnen Teilstücke zwischen den yields, werden atomar und isoliert ausgeführt. An dieser Stelle finde ich die Callbacks wesentlich weniger irreführend. Das yield funktioniert an der Stelle ja praktisch wie ein yield bei einem echten Thread...
Ich verstehe den Einwand nicht. Wenn du eine Instruktion ausführst ändert sich mindestens der Instruction-Pointer (jaja, es gibt einen degenerierten Fall) und damit der globale Zustand des Programms. Was wäre die Alternative?
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Alexander Kornrumpf hat geschrieben:
Nachtrag: Was ich sehr spannend an dem yield in node.js finde, ist dass der globale Programmzustand nach einem yield - so wie ich das verstehe - ggf. nicht mehr der ist, der vor dem yield vorlag. D.h. Programme mit yield laufen nicht mehr "atomar und isoliert", sondern nur die einzelnen Teilstücke zwischen den yields, werden atomar und isoliert ausgeführt. An dieser Stelle finde ich die Callbacks wesentlich weniger irreführend. Das yield funktioniert an der Stelle ja praktisch wie ein yield bei einem echten Thread...
Ich verstehe den Einwand nicht. Wenn du eine Instruktion ausführst ändert sich mindestens der Instruction-Pointer (jaja, es gibt einen degenerierten Fall) und damit der globale Zustand des Programms. Was wäre die Alternative?
Stell Dir mal folgendes (absichtlich falsch implementiert) vor (ob das wirklich so abläuft ist jetzt nur Spekulation):

Code: Alles auswählen

var lastLogin = null // "Globaler Programmzustand"
var lastLoginSuccess = false // "Globaler Programmzustand"

var myLogin = Promise.coroutine(function* (user,password) {
  lastLogin = user
  lastLoginSuccess = (yield rp.post( '.../myLoginService' , {form:{usr:user,pwd:password}})) == 'true'
});

Promise.all([myLogin('user1','password1'), myLogin('user2','password2')]).then(function (result) {
  if( lastLoginSuccess ) console.log(`Last successful login was ${lastLogin}`)
});
Vielleicht geht das nur mir so, aber ich finde da muss man doch zwei mal hinschauen bis man checkt, dass das lastLogin ggf. nicht zum lastLoginSuccess passt.

Bei

Code: Alles auswählen

var lastLogin = null // "Globaler Programmzustand"
var lastLoginSuccess = false // "Globaler Programmzustand"

var myLogin = Promise.coroutine(function* (user,password) {
  lastLogin = user
  yield rp.post( '.../myLoginService' , {form:{usr:user,pwd:password}})).then(function (result) {
     lastLoginSuccess  = result=='true'
  })
});

Promise.all([myLogin('user1','password1'), myLogin('user2','password2')]).then(function (result) {
  if( lastLoginSuccess ) console.log(`Last successful login was ${lastLogin}`)
});
finde ich das wesentlich leichter zu sehen.

edit: Ich musste den Code im zweiten Beispiel noch um ein yield ergänzen, damit das Promise erst nach Aufruf der externen Login-Methode fertig ist.
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Chromanoid hat geschrieben: Stell Dir mal folgendes (absichtlich falsch implementiert) vor ...
Ich stelle vor allem gerade fest, dass "ist dass der globale Programmzustand nach einem yield - so wie ich das verstehe - ggf. nicht mehr der ist, der vor dem yield vorlag." doppeldeutig ist. Es kommen mMn zwei Deutungen in Frage. Mit Callbacks ist es leichter zu erklären, aber für yield gilt es analog

1) (Darauf scheint dein Beispiel abzuzielen?!)
Ich kann mich innerhalb der Callback nicht darauf verlassen, dass der globale Zustand des Programms in dem Moment in dem die Callback aufgerufen wird der gleiche ist, wie in dem Moment in dem sie übergeben wird, weil er sich von außerhalb der Callback geändert haben könnte. Das ist richtig und wenn man es so hinschreibt ja offensichtlich. Wie man diesen Satz mit yield formulieren würde, weiß ich nicht, insofern stimme ich dir zu, dass die Callback-Variante das Problem eher herausschreit. Dem JavaScript Code in der Callback-Variante sieht man das aber mMn nicht irgendwie stärker an. Sprich Nachdenken muss man so oder so.

2) (Das hatte ich ursprünglich verstanden)
Ich kann mich außerhalb der Callback nicht darauf verlassen, dass die Callback, wenn sie aufgerufen wird, den globalen Zustand des Programms nicht verändert. Diese Lesart beschreibt mMn unvermeidbares und auch das was man (in diesem Fall) will, also kein Problem. Daher also meine Verwirrung.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Ah verstehe. Ja, ich meinte 1). Wenn man mit Callbacks arbeitet und ohne generator-Funktion (https://strongloop.com/strongblog/how-t ... use-cases/) weiß man, dass ein Codeblock { ... } mehr oder weniger atomar und isoliert ausgeführt wird. Mit mehr oder weniger atomar meine ich, dass man sich sicher sein kann, dass kein anderer Codeblock zwischenzeitlich ausgeführt wird. Dieses Wissen gilt nicht mehr wenn man yield in einem Codeblock benutzt, die Ausführung des Codeblocks wird an dieser Stelle ggf. unterbrochen. Das finde ich konzeptionell eine ziemlich weitreichende Veränderung des Verhaltens.
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Chromanoid hat geschrieben:Ah verstehe. Ja, ich meinte 1). Wenn man mit Callbacks arbeitet und ohne generator-Funktion (https://strongloop.com/strongblog/how-t ... use-cases/) weiß man, dass ein Codeblock { ... } mehr oder weniger atomar und isoliert ausgeführt wird. Mit mehr oder weniger atomar meine ich, dass man sich sicher sein, dass kein anderer Codeblock zwischenzeitlich ausgeführt wird.
In Javascript möglicherweise. In einem Szenario, das echte Nebenläufigkeit (vulgo "Threads") erlaubt, kann man sich dessen eben gerade nicht sicher sein.

Was unabhängig davon aus deinem Link deutlich wird ist dass man "Promises sind quasi Futures von hinten aufgezäumt." vielleicht besser als "... Generatoren von hinten aufgezäumt" formuliert hätte. Auch das wäre aber eine Aussage, die nur für diesen einen Anwendungsfall gelten würde.
Dieses Wissen gilt nicht mehr wenn man yield in einem Codeblock benutzt, die Ausführung des Codeblocks wird an dieser Stelle ggf. unterbrochen. Das finde ich eine ziemlich weitreichende Veränderung des Verhaltens.
Aber das ist doch gerade die definierende Eigenschaft eines yield, die mit Nebenläufigkeit gar nichts zu tun hat?!
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Alexander Kornrumpf hat geschrieben:In Javascript möglicherweise. In einem Szenario, das echte Nebenläufigkeit (vulgo "Threads") erlaubt, kann man sich dessen eben gerade nicht sicher sein.
Und genau das war dachte ich der Grund warum man lieber node.js benutzt. Weil eben kein Code nebenher läuft. Ich hatte mir die Arbeit mit node.js tatsächlich wie in einem Single-Thread vorgestellt nur ohne die Callbacks. Wenn man dann zwischendrin längere IO-Operationen hat, bräuchte man, um mehr Clients zu bedienen eben mehr Nodes... Die Lösung mit den Callbacks ist hier ein recht eleganter Weg um den Ausführungsfluss für andere Clients nicht zu behindern ohne allzu direkt auf die Komplexität verschränkter Anweisungsketten zu stoßen.
Alexander Kornrumpf hat geschrieben:Aber das ist doch gerade die definierende Eigenschaft eines yield, die mit Nebenläufigkeit gar nichts zu tun hat?!
Mit Nebenläufigkeit hat yield insofern etwas zu tun, dass man durch yield die Ausführung einer Anweisungskette mit einer anderen Anweisungskette verschränken kann. Man hat, sobald es ein yield gibt, zwei Pozesse deren Anweisungen nebenläufig ausgeführt werden. Wenn zwei Teil-Anweisungsketten keinen Zustand miteinander teilen, kann man diese Routinen sogar parallel ausführen. Bei den Callbacks ist das natürlich auch möglich, da verschränkt man dann aber nur Callbacks und nicht Teile dieser. Bei Callbacks muss man sich also nur Gedanken darüber machen, ob der globale Programmzustand zum Ende des Callbacks konsistent ist, bei Koroutinen mit yield, muss man das auch vor jeder yield-Anweisung sicherstellen.

Und damit ist man mMn schon viel näher an den Nebenläufigkeits-Problemen, die man mit node.js eigentlich durch die Paradigmen vermeiden will. Callbacks kann man sich leicht als isolierte Einheiten, die in beliebiger Reihenfolge ausgeführt werden, vorstellen, bei Koroutinen mit yield verliert man da IMO viel schneller den Überblick.
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Chromanoid hat geschrieben:Und genau das war dachte ich der Grund warum man lieber node.js benutzt. Weil eben kein Code nebenher läuft.
Ich kann da nur spekulieren, aber ich dachte der Grund wäre dass die Client-Entwickler dann die gleiche Sprache sprechen wie die Server-Entwickler.
Ich hatte mir die Arbeit mit node.js tatsächlich wie in einem Single-Thread vorgestellt nur ohne die Callbacks. Wenn man dann zwischendrin längere IO-Operationen hat, bräuchte man, um mehr Clients zu bedienen eben mehr Nodes...
Das ist ja im Prinzip das was PHP macht und weil das so einfach ist, hat es sich durchgesetzt, gegen alle Gründe die gegen PHP sprechen.
Die Lösung mit den Callbacks ist hier ein recht eleganter Weg um den Ausführungsfluss für andere Clients nicht zu behindern ohne allzu direkt auf die Komplexität verschränkter Anweisungsketten zu stoßen.
Ja, wobei das Hauptargument, auf das ich bisher gestoßen bin, Performance war und nicht Eleganz oder ease of use.

Mit Nebenläufigkeit hat yield insofern etwas zu tun, dass man durch yield die Ausführung einer Anweisungskette mit einer anderen Anweisungskette verschränken kann. Man hat, sobald es ein yield gibt, zwei Pozesse deren Anweisungen nebenläufig ausgeführt werden.
Hmm. Ich glaube nicht, dass das so stimmt. Man kann Generatoren vollkommen sequentiell in einem Prozess nutzen, wenn ich mich nicht gewaltig irre. Hier im Thread wurde einfach nur ein Pattern beschrieben wie man das mit Asynchronizität verknüpfen kann um eine schönere Syntax für letzteres zu bekommen.
Wenn zwei Teil-Anweisungsketten keinen Zustand miteinander teilen, kann man diese Routinen sogar parallel ausführen. Bei den Callbacks ist das natürlich auch möglich, da verschränkt man dann aber nur Callbacks und nicht Teile dieser.
Ich halte das für eine künstliche Unterscheidung. Sobald es mehr als einen Pfad durch ein Callback gibt kann man argumentieren dass nur "Teile" der Callback benutzt werden. Insbesondere, wenn es mehr als ein return statement gibt, oder gar ein bedingtes return aus einer Schleife heraus. In anderen Worten, für Sprachen, die sich nicht eisern an Prinzipien von Nikolaus Wirth halten stimmt deine vorherige Aussage "weiß man, dass ein Codeblock { ... } mehr oder weniger atomar und isoliert ausgeführt wird." eigentlich nicht. Man weiß nur dass ein Pfad zwischen dem Einstiegspunkt der Funktion und einem return die von dir genannte Eigenschaft aufweisen wird.
Bei Callbacks muss man sich also nur Gedanken darüber machen, ob der globale Programmzustand zum Ende des Callbacks konsistent ist, bei Koroutinen mit yield, muss man das auch vor jeder yield-Anweisung sicherstellen.
Genau wie vor return, s.o. yield ist return ja semantisch nicht ganz unähnlich. Die "Invertierung", die hier im Pattern angewendet wird kaschiert das leider.
Und damit ist man mMn schon viel näher an den Nebenläufigkeits-Problemen, die man mit node.js eigentlich durch die Paradigmen vermeiden will. Callbacks kann man sich leicht als isolierte Einheiten, die in beliebiger Reihenfolge ausgeführt werden, vorstellen, bei Koroutinen mit yield verliert man da IMO viel schneller den Überblick.
Huh? Ich dachte der ganze Sinn der Übung ist es, eine Reihenfolge über den Callbacks zu definieren (durch then bzw. nesting). Für Callbacks die in beliebiger Reihenfolge aufgerufen werden können, kann man sich den Zirkus auch sparen.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Alexander Kornrumpf hat geschrieben:
Mit Nebenläufigkeit hat yield insofern etwas zu tun, dass man durch yield die Ausführung einer Anweisungskette mit einer anderen Anweisungskette verschränken kann. Man hat, sobald es ein yield gibt, zwei Pozesse deren Anweisungen nebenläufig ausgeführt werden.
Hmm. Ich glaube nicht, dass das so stimmt. Man kann Generatoren vollkommen sequentiell in einem Prozess nutzen, wenn ich mich nicht gewaltig irre. Hier im Thread wurde einfach nur ein Pattern beschrieben wie man das mit Asynchronizität verknüpfen kann um eine schönere Syntax für letzteres zu bekommen.
Ich glaube hier ist wieder unser unterschiedliches Verständnis von Nebenläufigkeit das Problem. So wie ich das gelernt habe, gilt nur weil etwas sequentiell ausgeführt wird noch nicht, dass es nicht zwei Prozesse sind, die nebenläufig zu einander ausgeführt werden (siehe https://books.google.de/books?id=9dKYNO ... &q&f=false). Da die Generatoren in node.js ja scheinbar auch auf externe Ressourcen warten können und dann andere Callbacks ausgeführt werden, ist die Reihenfolge der Operationen nicht mehr total sondern nur noch partiell geordnet und dann spricht man meines Wissens nach von Nebenläufigkeit. Callbacks oder auch Interrupts sind deswegen meiner Interpretation nach auch eine Form der Nebenläufigkeit, es gibt nämlich keine totale Ordnung der Anweisungen, siehe https://www.informatik.uni-hamburg.de/T ... script.pdf S. 3 bzw. https://www4.cs.fau.de/Lehre/SS08/V_SPI ... /H2-A6.pdf S. 1. Aber das ist wirklich ein ziemliches Herumgereite auf den Definitionen und ich lasse mich gerne eines Besseren belehren.
Alexander Kornrumpf hat geschrieben:
Und damit ist man mMn schon viel näher an den Nebenläufigkeits-Problemen, die man mit node.js eigentlich durch die Paradigmen vermeiden will. Callbacks kann man sich leicht als isolierte Einheiten, die in beliebiger Reihenfolge ausgeführt werden, vorstellen, bei Koroutinen mit yield verliert man da IMO viel schneller den Überblick.
Huh? Ich dachte der ganze Sinn der Übung ist es, eine Reihenfolge über den Callbacks zu definieren (durch then bzw. nesting). Für Callbacks die in beliebiger Reihenfolge aufgerufen werden können, kann man sich den Zirkus auch sparen.
Ich meinte Callbacks bzw. Koroutinen, die von unterschiedlichen Client-Anfragen aus, einen globalen Zustand bearbeiten (das war das was ich mit der myLogin-Methode in meinem Beispiel andeuten wollte). Die Callbacks zu verschiedenen Client-Aufrufen und die im Aufruf dann registrierten Callbacks werden ja in ihrer Ausführungsreihenfolge je nach Antwortszeit externer Ressourcen gemixt. Genauso auch die Programmteile, die mit yield auf etwas warten.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Gerade auf Reddit gesehen: https://github.com/vasanthk/async-javascript/ "Evolution of Asynchronous JavaScript" passt zum Thema :)
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Chromanoid hat geschrieben:Ich glaube hier ist wieder unser unterschiedliches Verständnis von Nebenläufigkeit das Problem. So wie ich das gelernt habe, gilt nur weil etwas sequentiell ausgeführt wird noch nicht, dass es nicht zwei Prozesse sind, die nebenläufig zu einander ausgeführt werden (siehe https://books.google.de/books?id=9dKYNO ... &q&f=false). Da die Generatoren in node.js ja scheinbar auch auf externe Ressourcen warten können und dann andere Callbacks ausgeführt werden, ist die Reihenfolge der Operationen nicht mehr total sondern nur noch partiell geordnet und dann spricht man meines Wissens nach von Nebenläufigkeit. Callbacks oder auch Interrupts sind deswegen meiner Interpretation nach auch eine Form der Nebenläufigkeit, es gibt nämlich keine totale Ordnung der Anweisungen, siehe https://www.informatik.uni-hamburg.de/T ... script.pdf S. 3 bzw. https://www4.cs.fau.de/Lehre/SS08/V_SPI ... /H2-A6.pdf S. 1. Aber das ist wirklich ein ziemliches Herumgereite auf den Definitionen und ich lasse mich gerne eines Besseren belehren.

[...]

Genauso auch die Programmteile, die mit yield auf etwas warten.
Geschenkt. Ich schreib nochmal worauf ich hinaus will:

Yield hält den Generator an und übergibt einen Wert an den Aufrufer. Der Aufrufer kann einen Wert zurück an das yield geben und damit den Generator an dieser Stelle fortsetzen. Die Reihenfolge in der das passiert ist vollkommen deterministisch. Yield definiert also einen Punkt an dem der Generator angehalten und gestartet werden kann. Wenn du so willst ein Funktionskopf und return an derselben Stelle. Zu sagen yield würde auf etwas "warten" ist so wie zu sagen, eine Funktion würde darauf warten aufgerufen zu werden. Yield führt nicht mehr Nebenläufigkeit ein, als ein Funktionsaufruf, denn: Ein Generator ist keine Koroutine!

Was hier passiert ist, dass schlaue Menschen erkannt haben, dass man Koroutinen "elegant" mit Generator-Syntax implementieren kann. Was die Beispiele des Threaderstellers nicht zeigen, wohl aber dein Link zu Generatoren, ist dass nicht das yield auf ein asynchrones Ereignis wartet, sondern das andere Ende was dann, wenn das Ereignis eintritt, den Generator fortsetzt. Deswegen "invertiert" bzw. "von hinten aufgezäumt". Im Ergebnis sieht es so aus, als würde yield darauf warten. Die Magie liegt aber nicht im yield sondern in Coroutine. Alle Nebenläufigkeit (nach deiner Def.) wird durch Coroutine (eine Funktion des Promise-API) eingeführt, nicht durch den Generator. Es ist ein Pattern, das Generatoren verwendet, kein Sprachkonstrukt.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Ah, ich verstehe jetzt was Du meinst. Während das Sprachkonstrukt des Generators die Möglichkeit bereitstellt den Programmfluss einer Routine (die Generator-Routine) zu unterbrechen und später wieder aufzunehmen (eine wichtige Voraussetzung für Koroutinen), muss die Ausführung der verschiedenen Generator-Routinen noch in Bezug auf nebenläufige Ereignisse orchestriert werden, damit sie als nebenläufige Koroutinen funktionieren.
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Chromanoid hat geschrieben:Ah, ich verstehe jetzt was Du meinst. Während das Sprachkonstrukt des Generators die Möglichkeit bereitstellt den Programmfluss einer Routine (die Generator-Routine) zu unterbrechen und später wieder aufzunehmen (eine wichtige Voraussetzung für Koroutinen), muss die Ausführung der verschiedenen Generator-Routinen noch in Bezug auf nebenläufige Ereignisse orchestriert werden, damit sie als nebenläufige Koroutinen funktionieren.
Ja. Um nochmal auf Callbacks vs. Generatoren zurückzukommen, Ich glaube, dass die "gotchas" bei der Sache aus der Orchestrierung kommen, die das sagst du richtig, trotz single-threadedness, nicht transparent funktioniert (und wahrscheinlich auch nicht transparent sein kann, ich wüsste nicht wie). Die Orchestrierung ist aber für die Callbacks im Prinzip die gleiche, mit den gleichen "gotchas".

Dass man unabhängig davon eine Syntax subjektiv zugänglicher finden kann als eine andere bleibt davon natürlich unberührt. Generatoren sind sicher etwas worüber man mal einen Nachmittag nachdenken kann bevor man sie naiv einsetzt. Insofern nochmal danke, dass du mich bei besagtem Nachdenken unterstützt hast.
Benutzeravatar
Chromanoid
Moderator
Beiträge: 4254
Registriert: 16.10.2002, 19:39
Echter Name: Christian Kulenkampff
Wohnort: Lüneburg

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Chromanoid »

Ebenso Danke! Ohne Deine Hinweise, hätte ich von der Umsetzung eine falsche Vorstellung behalten. Das Generator-Konstrukt war mir vor dem Thema hier auch noch unbekannt. In Python und wohl noch einigen anderen Sprachen gibt es ähnliche Implementierungen von Koroutinen damit. Echte parallele Verarbeitung ist mir da irgendwie lieber. Das Argument aus http://blog.stevenedouard.com/asynchron ... ncurrency/ für node.js, bei IO als Flaschenhals, hatte mich noch etwas irritiert:
C# runtime doesn’t scale as well to I/O-bound tasks like loading washing machines, primarily due to the overhead of context switching of threads, it does scale better at computationally intensive, CPU-bound tasks.
Dem scheint wirklich so zu sein, da .NET wohl einen Threadpool für IO-Aufgaben hat (siehe http://stackoverflow.com/questions/8905 ... e-js-possi). Ich glaube Java ist an der Stelle rudimentärer und sollte dieses Problem nicht haben.
KayZ
Beiträge: 47
Registriert: 05.01.2015, 23:46
Echter Name: Dennis

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von KayZ »

@antisteo: Ich muss einfach mal Dart in den Raum werfen. Bei Dart ist das ganze async-Zeug direkt Teil der Programmiersprache und aller APIs ohne dass es unterschiedliche Implementationen durch verschiedene Libraries gibt. Und sie decken wirklich alles ab, also nicht nur Futures sondern auch Streams (asynchrone Listen) und es gibt auch await, so dass man dort nicht mal die beispielhaft genannte 100 fache Verkettung von Futures/Promises machen muss, sondern man kann es nahezu wie synchronen Code schreiben.

Also zum Beispiel statt:

Code: Alles auswählen

HttpServer.bind('127.0.0.1', 4444)
  .then((server) => print('${server.isBroadcast}'))
  .catchError(print);
kann man auch:

Code: Alles auswählen

try {
  var server = await HttpServer.bind('127.0.0.1', 4444);
  print('${server.isBroadcast}');
} catch (e) {
  print(e);
}
schreiben.

https://www.dartlang.org/docs/tutorials/futures/
https://www.dartlang.org/docs/tutorials/streams/

Im zweiten Link sieht man das ganze anhand eines asynchronen Generators dessen Werte innerhalb einer await for Schleife abgerufen werden und dessen Ergebnis eine Future ist. Der Code liest sich, abgesehen von ein paar keywords, wie synchroner Code.

Warum ich Dart in den Raum werfe? Man kann Dart für clientseitige als auch für serverseitige Anwendungen nutzen. Im Browser kompiliert man es nach Javascript (im Normalfall performanter als handgeschriebenes JS und durch Treeshaking (nur wirklich verwendeter Code wird in JS umgewandelt) teilweise auch kleiner als eine JS-Anwendung mit x-Libs die man so benötigt um halbwegs vernünftiges JS schreiben zu können) und auf dem Server läuft es nativ. Wie in Javascript gibt es in Dart nur einen Thread, man kann aber mehrere "Isolates" erzeugen die allerdings nur über Nachrichten miteinander kommunizieren können (es gibt keinen globalen Speicherbereich in dem mehrere Isolates rumhantieren und sich gegenseitig in die Quere kommen können, also keine concurrent modification exceptions möglich, wie man sie z.B. aus Java kennt).
Alexander Kornrumpf
Moderator
Beiträge: 2106
Registriert: 25.02.2009, 13:37

Re: Die Lösung für nebenläufiges Denken: Promises

Beitrag von Alexander Kornrumpf »

Hier macht es jemand live mit python vor:

http://pyvideo.org/video/3432/python-co ... nd-up-live
Antworten