(C++) Coroutines - Motivation?



  • Eisflamme schrieb:

    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?

    Für das Beispiel bin ich vereinfacht davon ausgegangen das man die Datei komplett in einen Buffer liest und dann anzeigt. Beim Streaming geht man vom Konzept aber in die gleiche Richtung. Aus der GUI stoße ich einen Lesevorgang an und rufe regelmäßig die Funktion zum Daten abholen auf. Jedes mal wenn ich die Funktion aufrufe, gebe ich die Kontrolle gezielt ab und die Lese Routine arbeitet. Kehrt die Lese Routine zurück, bekommt die GUI wieder die Kontrolle und kann sich mit den bisherigen Daten aktualisieren.

    Unterschied zu den Coroutines ist, dass die Lese Routine jedes mal komplett beendet und neu gestartet wird. Dabei geht dann auch jeder lokale Zustand verloren. Man muss also jedes mal die nötigen Daten bzw. den Objektkontext mitgeben. Eine Coroutine kann Daten liefern und später dort weiter machen wo sie gestoppt hat, ohne den lokalen Kontext zu verlieren.



  • Eisflamme schrieb:

    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?

    Jein. Klar geht es um Threads aber zB Daten von Netzwerkkarte senden ist zwar prinzipiell ein Thread genauso wie Daten auf Festplatte schreiben - aber eben tief im System hinterlegt und die Synchronisierung findet hier eh sowieso immer statt.

    Coroutines sind einfach dafür da, wenn man eine Ausführung unterbrechen will und fortsetzen ohne echte Threads verwenden zu wollen. Threads haben gewisse Kosten.

    In C# verwende ich Coroutines zB sehr oft beim lesen aus einer "Datenbank" (wobei Datenbank hier abstrakt zu sehen ist: etwas dass mir Daten liefert) und ich verarbeite die Zeile für Zeile und lese Zeile für Zeile in eine Collection. Und immer wenn ich die neue Zeile brauche, hole ich sie mir adhoc aus der Datenbank - das ganze ist eben sehr simpel und der Read Cache übernimmt die Performance optimierung.

    Prinzipiell sind Coroutines dann sehr praktisch wenn man blockende Funktionen hat. zB readFromFile oder writeToNetwork. Dann macht man etwas anderes während das System hier den Rest handelt. Bedenke: ein readFromFile wird immer über den IO Thread des Systems fahren.



  • Die asynchronen IOs die für Coroutines & Co nötig sind sind auch nicht ganz gratis.
    Vermutlich billiger als Blocking-IO + Worker-Thread, aber sicher nicht gratis.

    Bzw. je nachdem welche Library/Implementierung man für die asynchronen IOs verwendet sogar teurer. Ich hab' mal in die Implementierung der asynchronen Sockets Funktionen im .NET Framework reingeguckt, da wurde mir schwummerig vor Augen 😉

    @Shade Of Mine
    Hast du zufällig Code zur Hand den du herzeigen kannst (und darfst)? Würde mich interessieren wie das aussieht.
    Ich kenne etwas sehr ähnliches aus C#, nämlich indem man den Lese-Code in eine Generator-Funktion reinstopft. Wobei ja vom Compiler automatisch eine Coroutine erzeugt wird. Bloss die blockt nach wie vor in IO Calls.
    Ansonsten kenne ich noch async/await , aber das wird recht schnell recht hässlich.
    Würde mich also interessieren wie das bei dir aussieht.



  • Eisflamme schrieb:

    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

    Da ist keine große Magie dabei. Es ist einfach eine andere Syntax, um das aufzuschreiben, was vorher schon möglich war:

    Callback Hell:

    struct async_stuff
    {
    	void start()
    	{
    		socket.async_read(buffer, [this](boost::system::error_code, std::size_t)
    		{
    			start();
    		});
    	}
    };
    

    Coroutine:

    struct async_stuff
    {
    	void start()
    	{
    		boost::asio::spawn(io, [](boost::asio::yield_context yield)
    		{
    			for (;;)
    			{
    				boost::system::error_code ec;
    				std::size_t s = socket.async_read(buffer, yield[ec]);
    			}
    		});
    	}
    };
    

    Man sieht vielleicht, dass start() einem goto recht ähnlich sieht. Bei der Coroutine hingegen kann man eine höhere Kontrollstruktur benutzen.



  • hustbaer schrieb:

    Hast du zufällig Code zur Hand den du herzeigen kannst (und darfst)? Würde mich interessieren wie das aussieht.
    Ich kenne etwas sehr ähnliches aus C#, nämlich indem man den Lese-Code in eine Generator-Funktion reinstopft. Wobei ja vom Compiler automatisch eine Coroutine erzeugt wird. Bloss die blockt nach wie vor in IO Calls.

    Prinzipiell sind es ja keine echten Coroutines sondern eigentlich nur yield calls.

    Es ist etwa so was:

    public IEnumerable<Data> GetData()
    {
    	EntityManager e = new EntityManager();
    	while(e.hasNext()) {
    		yield return e.current();
    	}
    }
    

    Das coole dabei ist eigentlich, dass es in der Collection versteckt ist was da eigentlich passiert. Sowas sind eigentlich die einzigen Fälle von Coroutunes die ich verwende.



  • @Shade Of Mine
    Danke für das Beispiel - genau so kenne ich das auch.
    Ist aber eben eines von den Beispielen wo nichts "ent-blockt" wird -- Nebenläufigkeit oder Time-Slicing innerhalb eines Threads erreicht man dadurch zwar schon, aber nicht so dass man die "Löcher" wo der Thread auf IO wartet auffüllen könnte.



  • TyRoXx schrieb:

    Coroutine:

    struct async_stuff
    {
    	void start()
    	{
    		boost::asio::spawn(io, [](boost::asio::yield_context yield)
    		{
    			for (;;)
    			{
    				boost::system::error_code ec;
    				std::size_t s = socket.async_read(buffer, yield[ec]);
    			}
    		});
    	}
    };
    

    Weisst du wie das intern funktioniert?
    Ich sehe da keine Möglichkeit ohne schwarze Compiler- und/oder OS-Magic.



  • Boost.Coroutine baut auf Boost.Context auf, das die CPU-Register sichern und wiederherstellen kann und eben unter anderem den Stack Pointer umbiegt. Der Teil ist natürlich plattformspezifisch in Assembler geschrieben. Der Stack kann von malloc kommen oder mit mmap -Spielchen gegen einen Überlauf geschützt werden.

    asio::spawn verwendet Boost Coroutine und bietet den yield_context an, der mit den async_* -Operationen von Asio kompatibel ist.

    Das benutzt sich in etwa wie ein Kernel-Thread. mutex und so gibt es hierfür (noch) nicht, was aber nicht unbedingt ein Nachteil ist ;). Der Vorteil ist, dass die Kontextwechsel ohne Umweg über den Kernel funktionieren und damit schneller sind. Konkret wird ein run -Thread von epoll oder ähnlichem geweckt und Asio lässt dann N Coroutinen nacheinander ein Stück laufen. Ein Kernel-Thread pro Vorgang würde bedeuten, dass N Kernel-Threads geweckt werden müssten. Man spart hier (N-1) mal den Kernel-zu-Userspace-und-am-Ende-wieder-zurück-Overhead.

    Mit richtiger Compiler-Unterstützung könnte man jetzt auch noch den Speicherverbrauch der Coroutinen reduzieren und die Kontextwechsel optimieren.



  • TyRoXx schrieb:

    Boost.Coroutine baut auf Boost.Context auf, das die CPU-Register sichern und wiederherstellen kann und eben unter anderem den Stack Pointer umbiegt. Der Teil ist natürlich plattformspezifisch in Assembler geschrieben. Der Stack kann von malloc kommen oder mit mmap -Spielchen gegen einen Überlauf geschützt werden.

    Danke für die Info.

    TyRoXx schrieb:

    Konkret wird ein run -Thread von epoll oder ähnlichem geweckt und Asio lässt dann N Coroutinen nacheinander ein Stück laufen.

    Verstehe ich jetzt nicht ganz. Sollte man die Dinger nicht laufen lassen so lange bis keine mehr was zu tun hat?

    Oder kann Boost.Coroutine einfach nicht feststellen dass eine bestimmte Coroutine gerade nix zu tun hat, und muss deswegen pollen? In dem Fall verstehe ich die Begeisterung nicht ganz. Damit ersetzt du halbwegs effizientes "reactive IO" durch Polling. Hmpf.



  • hustbaer schrieb:

    TyRoXx schrieb:

    Konkret wird ein run -Thread von epoll oder ähnlichem geweckt und Asio lässt dann N Coroutinen nacheinander ein Stück laufen.

    Verstehe ich jetzt nicht ganz. Sollte man die Dinger nicht laufen lassen so lange bis keine mehr was zu tun hat?

    Oder kann Boost.Coroutine einfach nicht feststellen dass eine bestimmte Coroutine gerade nix zu tun hat, und muss deswegen pollen? In dem Fall verstehe ich die Begeisterung nicht ganz. Damit ersetzt du halbwegs effizientes "reactive IO" durch Polling. Hmpf.

    Ich meinte damit, dass die solange läuft, bis sie wieder auf etwas wartet oder sich beendet.



  • OKok
    Ist quasi ein bisschen so wie async/await von C# - nur halt mit gänzlich anderem Unterbau.

    Ich hoffe aber sehr dass das als Core Language Erweiterung in C++ integriert wird, wie es bei C# gemacht wurde. Und nicht als Library.



  • hustbaer schrieb:

    OKok
    Ist quasi ein bisschen so wie async/await von C# - nur halt mit gänzlich anderem Unterbau.

    Ich hoffe aber sehr dass das als Core Language Erweiterung in C++ integriert wird, wie es bei C# gemacht wurde. Und nicht als Library.

    In einem video von der CPPcon haben die verantwortlichen fuer concurrency mal gesagt, dass neue keywords geplant sind. Du koenntest also Glueck haben.


Anmelden zum Antworten