(C++) Coroutines - Motivation?



  • Hi,

    da Coroutines in boost weiter verbessert wurden, habe ich mich jetzt auch mal ein wenig damit beschäftigt.

    Was mir noch fehlt ist eine gute Motivation dafür in einem Satz. Ich hab Wiki-Artikel, die boost-Motivation uvm. durchgelesen, bin aber immer noch nicht überzeugt. Ich habe den Eindruck, da steckt ein Potential hinter, das ich noch nicht wirklich erkannt habe.

    Mir fehlt auch noch die einfache Information, was genau der HAUPTVorteil ist?
    - Performance?
    - Sauberer Code?
    - Neue Möglichkeiten des Programmflusses, die es vorher nicht gab?

    Vermutlich steckt dahinter ja eine neue Art Programme zu schreiben. Doch kann ich damit beispielsweise auch auf einfache Weise multithreaded-Anwendungen schreiben, bei denen ich ich vorher nicht parallelisieren wollte, weil es zu kompliziert war? Oder was ist der große Witz?

    Viele Grüße
    Eisflamme



  • Mich würden da auch mal Use Cases dafür interessieren. Der einzige Use Case abseits der langweiligen yield Beispiele, den ich bisher gefunden habe, ist das hier:

    http://rethinkdb.com/blog/improving-a-large-c-project-with-coroutines/

    Und da bin ich mir auch nicht sicher, ob das wirklich ein sinnvoller Use Case war. Ich hab eher das Gefühl, dass der Nutzen sehr sehr marginal ist und man schon Mühe hat, 2-3 sinnvolle Einsatzmöglichkeiten zu finden.
    Ich hatte auch mal einen Fall, wo ich das Einlesen von XML Daten unschön fand und mir überlegt habe, da mal zum Test Coroutines einzusetzen. Habs dann aber doch nicht gemacht, weils im Endeffekt egal war... Dann ist das Einlesen der XML hässlich, egal, ist in einer cpp gekapselt, die keinen interessiert, war schnell runtergetippt und schnell genug (und mit Coroutines wärs sicherlich langsamer gewesen). Ansonsten bin ich bisher nie über irgendwas gestolpert, wo sich das angeboten hätte.



  • Eisflamme schrieb:

    Hi,

    da Coroutines in boost weiter verbessert wurden, habe ich mich jetzt auch mal ein wenig damit beschäftigt.

    Was mir noch fehlt ist eine gute Motivation dafür in einem Satz. Ich hab Wiki-Artikel, die boost-Motivation uvm. durchgelesen, bin aber immer noch nicht überzeugt. Ich habe den Eindruck, da steckt ein Potential hinter, das ich noch nicht wirklich erkannt habe.

    Mir fehlt auch noch die einfache Information, was genau der HAUPTVorteil ist?
    - Performance?
    - Sauberer Code?
    - Neue Möglichkeiten des Programmflusses, die es vorher nicht gab?

    Vermutlich steckt dahinter ja eine neue Art Programme zu schreiben. Doch kann ich damit beispielsweise auch auf einfache Weise multithreaded-Anwendungen schreiben, bei denen ich ich vorher nicht parallelisieren wollte, weil es zu kompliziert war? Oder was ist der große Witz?

    Viele Grüße
    Eisflamme

    Der entscheidende Vorteil von Coroutines ist die Implementation eines "kooperativen Multitasking", d.h. du kannst mehrere Threads/Tasks quasi-parallel ausführen, ohne dass die CPU per Timer-Interrupt gewaltsam einen Context-Switch machen muss, was Rechenzeit frisst. Der Programmierer entscheidest, wann umgeschaltet wird. Windows-OS bietet bereits seit NT 3.51 Coroutines an. Das nennt sich dort "Fiber". 🙂



  • Ich denke Coroutines machen vor allem da Sinn, wo es sehr viele IO lastige bzw. wartende Tasks gibt, die durchgeführt werden sollen. Häufig findet man diese bei Webservern: Eingabe parsen, Datenbank query, auf Daten warten (yield), Antwort generieren, Antwort senden (yield). So können sehr viele Anfragen erstmal schnell angenommen und dann nach und nach beantwortet werden. Die Verwaltung/Synchronisierung entfällt da zu großen Teilen, weil nicht einfach irgendwo die Ausführung unterbrochen wird.

    Andromeda schrieb:

    Der entscheidende Vorteil von Coroutines ist die Implementation eines "kooperativen Multitasking", d.h. du kannst mehrere Threads/Tasks quasi-parallel ausführen, ohne dass die CPU per Timer-Interrupt gewaltsam einen Context-Switch machen muss, was Rechenzeit frisst. Der Programmierer entscheidest, wann umgeschaltet wird.

    Man braucht auch um kritische Abschnitte nicht mehr zu synchronisieren. Das ist vermutlich Performance technisch nicht so entscheidend, aber erleichtert dem Programmierer die Arbeit ungemein.



  • quasi-parallel

    Wir sprechen hier aber vom echten Multithreading, also es werden mehrere CPU-Kerne genutzt?

    Demnach haben wir wirklich GAR keinen Context Switch mehr, also nur noch den Overhead eines gewöhnlichen Funktionsaufrufs? Dann könnte man ja "Mikro-Parallelisierungen" machen, die sonst eben wegen Context Switch nicht gingen.

    Logische Konsequenz wäre aber dann doch die Performance, die man erreicht, die man bei einem normalen synchronen Programm nicht hätte (und die man nicht in Threads auslagert, weil das auch einen höheren Entwicklungsaufwand haben könnte).

    Das verspricht dann viel, ist ja eine große Blackbox. Demnach ist das wirklich BS-seitig implementiert, man kann also mit Threads kein identisches Pendant schaffen?



  • Eisflamme schrieb:

    quasi-parallel

    Wir sprechen hier aber vom echten Multithreading, also es werden mehrere CPU-Kerne genutzt?

    Nein. Coroutines laufen in einem Thread.



  • Eisflamme schrieb:

    quasi-parallel

    Wir sprechen hier aber vom echten Multithreading, also es werden mehrere CPU-Kerne genutzt?

    Nein, die Coroutines teilen sich einen Kern. Es hindert dich aber nichts daran auf jeden Kern einen Satz an Coroutines laufen zu lassen. Zwischen den Coroutinen auf dem gleichen Kern braucht man nicht synchronisieren. Geht es darüber hinaus allerdings schon. So könnte man sich die Architektur geschachtelt aufbauen.

    Eisflamme schrieb:

    Logische Konsequenz wäre aber dann doch die Performance, die man erreicht, die man bei einem normalen synchronen Programm nicht hätte (und die man nicht in Threads auslagert, weil das auch einen höheren Entwicklungsaufwand haben könnte).

    Die bessere Performance im Vergleich zu einem synchronen Programm wird dadurch erreicht, dass während wartenden Aktionen (z.B. IO) der Prozessor weiter arbeiten kann.

    Eisflamme schrieb:

    Das verspricht dann viel, ist ja eine große Blackbox. Demnach ist das wirklich BS-seitig implementiert, man kann also mit Threads kein identisches Pendant schaffen?

    Vielleicht verstehe ich den Satz falsch, aber Coroutines werden im User Space Implementiert. Dadurch hat man ja erst die volle Kontrolle über das was passiert und kann sich generische Context Switches, die einfach alles Sichern und Invalidieren müssen, sparen.



  • Aber IO-Interrupts mal außen vor gelassen verstehe ich dann nicht, wie man Performance steigern kann. Wenn ich etwas tun kann, WÄHREND der Prozessor arbeitet, muss er das doch wohl in einem anderen Thread tun?



  • Eisflamme schrieb:

    Aber IO-Interrupts mal außen vor gelassen verstehe ich dann nicht, wie man Performance steigern kann. Wenn ich etwas tun kann, WÄHREND der Prozessor arbeitet, muss er das doch wohl in einem anderen Thread tun?

    Das ist richtig. Bei reinen Berechnungen gewinnst du keine Performance. Es geht nur darum den Prozessor während Wartezeiten sinnvoll und mit möglichst wenig Overhead mit einem anderen Task weiter beschäftigen zu können. Deswegen die Beispiele mit den vielen IO lastigen Tasks.



  • Eisflamme schrieb:

    quasi-parallel

    Wir sprechen hier aber vom echten Multithreading, also es werden mehrere CPU-Kerne genutzt?

    Demnach haben wir wirklich GAR keinen Context Switch mehr, also nur noch den Overhead eines gewöhnlichen Funktionsaufrufs? Dann könnte man ja "Mikro-Parallelisierungen" machen, die sonst eben wegen Context Switch nicht gingen.

    Logische Konsequenz wäre aber dann doch die Performance, die man erreicht, die man bei einem normalen synchronen Programm nicht hätte (und die man nicht in Threads auslagert, weil das auch einen höheren Entwicklungsaufwand haben könnte).

    Das verspricht dann viel, ist ja eine große Blackbox. Demnach ist das wirklich BS-seitig implementiert, man kann also mit Threads kein identisches Pendant schaffen?

    Coroutines werden IMHO nicht auf mehrere (logische) Threads verteilt. Eine Technik um mehrere CPUs in einem Programm gleichzeitig zu nutzen, ohne dass zwischen ihnen hin- und hergeschaltet wird, ist leider noch nicht erfunden worden. :p

    Multithreading ist kein Gewinn hinsichtlich Ausführungsgeschwindigkeit. Erst recht nicht auf einer Single-Core-CPU. Nur scheinbar, weil Software oft auf irgendwas wartet, während parallel dazu andere Aufgaben ausgeführt werden können. Man kann ein Programm aber auch immer so programmieren, dass es nie warten muss bzw. immer etwas tut, das gerade ansteht. Das läuft dann auf Homegrown-Coroutines hinaus. 😉 Aber das ist viel schwieriger zu machen, als eine Multithreading-Anwendung inklusive der Synchronisationsprobleme.



  • Eisflamme schrieb:

    Aber IO-Interrupts mal außen vor gelassen verstehe ich dann nicht, wie man Performance steigern kann. Wenn ich etwas tun kann, WÄHREND der Prozessor arbeitet, muss er das doch wohl in einem anderen Thread tun?

    In erster Linie bieten coroutines einen Uebersichtlichkeitsvorteil. Man kann aufwendign Berechnungen durchfuehren und mal eben schnell zum gui rueberwechseln und trotzdem den Code der beiden Ausfuehrungsstraenge sauber getrennt halten, statt sie manuell immer mal wieder abwechselnd aufzurufen.
    Sie sind in so fern minimal schneller als OS-threads, da der Wechsel weniger Overhead hat und man andererseits auch auf synchronisierungsmechanismen wie mutexe verzichten kann.



  • Marthog schrieb:

    Sie sind in so fern minimal schneller als OS-threads, da der Wechsel weniger Overhead hat und man andererseits auch auf synchronisierungsmechanismen wie mutexe verzichten kann.

    Nicht die synchronisation dabei bedenken. Threads brauchen immer synchronisation, Coroutines nicht. Das ist nicht nur ein reiner Geschwindigkeitsunterschied sondern auch ein enormer Komplexitäts/Fehleranfälligkeits Unterschied.





  • Boost.Coroutine ist ein technischer Zwischenschritt. Es ist das, was ohne neue Sprach-Features möglich ist. Ein Callstack, der mit etwas Syntactic Sugar ohne Hilfe des Kernels betreten und verlassen werden kann.

    Was man eigentlich will, ist Compiler-Unterstützung. Im Idealfall kann der Compiler statisch berechnen, wie groß der Stack werden kann bzw den gleichen Code generieren, die man durch Callback Hell erhalten hätte. Der Speicherverbrauch wäre entsprechend gering. Boost Coroutine fordert AFAIK immer mindestens eine physikalische Speicherseite an (4096 Bytes oder sogar mehr) und den Rest des Stacks virtuell, sodass er wachsen kann (zumindest bei GCC ab 4.7). Auch das Betreten und Verlassen des Callstacks kann nicht für den Einzelfall vom Compiler optimiert werden, weil das in Boost.Context mit Assembler pro Plattform geschrieben wurde. Visual Studio 2015 hat schon erste, experimentelle Spracherweiterungen.

    Die Dokumentation von Boost.Coroutine ist äußerst mies und faselt nur von irgendwelchen XML-Parsern. Viel interessanter ist, was Asio damit macht. Coroutinen passen sehr gut zum io_service und dem async_result -Konzept. Das ist viel besser lesbar als die alte Asio-Callback-Hell. Bei I/O fällt die fette Boost Coroutine auch nicht mehr so auf.

    Man steigert damit nie die Geschwindigkeit, sondern nur die Les- und Schreibbarkeit.



  • Und irgendwo schon mal in der Praxis benutzt?



  • Nicht die synchronisation dabei bedenken. Threads brauchen immer synchronisation, Coroutines nicht. Das ist nicht nur ein reiner Geschwindigkeitsunterschied sondern auch ein enormer Komplexitäts/Fehleranfälligkeits Unterschied.

    Also doch auch ein Geschwindigkeitsvorteil? Wenn Synchronisierung plötzlich eine Rolle spielt, haben wir ja doch Multithreading im Spiel? Oder geht es dir auch um I/O.

    TyRoXx:
    Ehrlich gesagt verstehe ich bei deinem Beitrag nicht besonders viel, ich hab generell ein paar Wissenslücken. Was ist denn die Quintessenz? Coroutines könn(t)en den Callstack entschlacken. Wenn die Performance aber nicht steigt, was nützt mir das dann? Und Lesbarkeit... die hat ja mit dem Call-Stack wiederum nichts zu tun.

    Irgendwie überzeugen mich diese Coroutines immer noch nicht. IO mache ich sowieso über irgendwelche Libraries, da sehe ich also keinen Nutzen



  • Eisflamme schrieb:

    Also doch auch ein Geschwindigkeitsvorteil? Wenn Synchronisierung plötzlich eine Rolle spielt, haben wir ja doch Multithreading im Spiel? Oder geht es dir auch um I/O.

    Im Gegenteil. Synchronisieren spielt hier keine Rolle. Wenn du mehrere Aktionen gleichzeitig bzw. abwechselnd ausfuehren willst, brauchst du normalerweise verschiedene Ausfuehrungsstraenge (klassisches Beispiel: GUI-events und lange Berechnung gleichzeitig).
    Du kannst normale threads nehmen, aber dann musst du synchronisieren und dabei hoellisch aufpassen, weil an jeder Stelle im Code der threadwechsel stattfinden kann. Mit coroutines kannst du selber angeben, wann du wechseln willst und laesst es an kritischen Stellen einfach bleiben. Dadurch hat der Wechsel geringeren overhead als echte threads. Da laeuft trotzdem nichts in parallel.



  • Eisflamme schrieb:

    Irgendwie überzeugen mich diese Coroutines immer noch nicht. IO mache ich sowieso über irgendwelche Libraries, da sehe ich also keinen Nutzen

    Vermutlich weil du deine Coroutines selbst programmierst, ohne es zu wissen.

    IO.write (some_megabytes)   // non-blocking
    while (not IO.write_complete)
    {
       do_other_stuff();
    }
    

    😉



  • Mit einem konkreten Beispiel ist das Thema vielleicht leichter zu erklären:

    Nehmen wir einen GUI Texteditor. Der kann nicht mehr als eine Datei einlesen und den Inhalt darstellen. Es ergeben sich 3 Mögliche Implementierungen:

    1. Synchron: Wenn auf Öffnen geklickt wird, wird direkt die Datei eingelesen und das UI Element mit Text gefüllt. Wir sind im GUI Thread, brauchen also keine Synchronisation oder ähnliches. Dafür reagiert die GUI nicht mehr bis die Datei komplett eingelesen wurde.

    2. Threads: Wenn auf Öffnen geklickt wird, wird ein Thread erzeugt. Dieser Thread liest die Datei ein, kann dann aber nicht direkt in das GUI Element schreiben, da dies nur aus dem GUI Thread möglich ist. Also brauchen wir Synchronisation. Häufig gibt es da Invoke Funktionen um Code im GUI Thread auszuführen. Man muss also grundsätzlich immer darauf achten in welchem Thread man gerade ist und das Invoke ist natürlich nicht ohne Overhead, da wir durch das hin und her mehrere Context switches in den Kernel etc. haben.

    3. Coroutines: Wenn auf Öffnen geklick wird, wird die Coroutine zum Datei einlesen angestoßen. Sobald die Coroutine auf IO trifft, gibt sie die Kontrolle ab und die GUI bekommt wieder CPU Zeit. Die GUI bleibt im Gegensatz zu Fall 1 also nicht stehen. Es spielen sich beide Coroutines die Kontrolle hin und her bis die Datei eingelesen wurde. Dies geschieht im User Space und ist daher nicht annähernd so teuer wie im Thread Fall. Anschließend schreibt die Lese Coroutine die Daten direkt in das UI Element. Das ist möglich, weil es der gleiche Thread ist und die GUI in einem wohldefinierten Zustand die Kontrolle abgegeben hat.



  • Andromeda:
    Ja, Koroutinen nutzen Polling. Aber was ich hier polle, sieht ja Dinge, die u.U. in einem anderen Thread geschehen sind. Anderenfalls finde ich die Event-Queue nur mäßig sinnvoll. Aber stimmt wahrscheinlich, eine One-Thread-Event-Queue ist ein klassisches Beispiel für Coroutines?

    Tobiking2:
    Zu Fall 3: Verstehe ich theoretisch schon, praktisch aber nicht. Ich streame Dateien doch normalerweise und lese in diesem Prozess die Daten in eine Datenstruktur, da gibt's also immer mal wieder kleine IO-Abschnitte.

    Alternativ kann ich die gesamte Datei auf einmal lesen, z.B. über einen string, meinst du das? Aber das geht ja über die normalen fstreams nicht, richtig?


Anmelden zum Antworten