(C++) Coroutines - Motivation?



  • 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