Ahoi,
ich arbeite schon länger an einer Bibliothek namens CoCo, Coroutinen für
microController auf Basis von C++ 20. Das verhält sich wie ein
Multitasking-Betriebssystem, jedoch nur kooperatives Multitasking, kein
preemptives Multitasking. Für zeitkritische Aufgaben reichen oft auch
Interrupts, so dass man damit eigentlich gut "bedient" ist, daher will
ich es mal kurz vorstellen. LEDs blinken lassen macht man so:
Coroutine task1(Loop &loop) {
while (true) {
debug::toggleRed();
co_await loop.sleep(200ms);
}
}
Coroutine task1(Loop &loop) {
while (true) {
debug::toggleGreen();
co_await loop.sleep(350ms);
}
}
int main() {
task1(drivers.loop);
task2(drivers.loop);
drivers.loop.run();
}
Das Main-Programm wird normal ausgeführt, task1 unterbricht jedoch beim
ersten co_await (hängt sich in die Event-Loop) so dass es mit task2
weitergeht, was auch bei co_await unterbricht. Danach wird die Event
Loop ausgeführt. Diese weckt die beiden Coroutinen nach den jeweiligen
Zeiten auf, die die LEDs umschalten und sich dann wieder schlafen legen.
Der Stack von task1 und task2 wird vom Compiler automatisch entweder
statisch oder auf dem Heap angelegt, da muss man sich nicht drum
kümmern, denn die Coroutinen "leben" ja dauerhaft weiter. "drivers" ist
eine Struktur, die man für jede Kombination aus Mikrocontroller und
Dev-Board anlegt und die in diesem Fall die Event-Loop enthählt
(Windows, STM32 oder nRF52). Hier ein github-Link:
https://github.com/Jochen0x90h/coco-loop/blob/main/test/LoopTest.cpp
Monochome Displays gehen auch schon:
https://github.com/Jochen0x90h/coco-mono-display sowie Funkübertragung
mit nRF5284. Es gibt eine ganze Reihe von coco-Bibliotheken, die man
sich mit conan als Abhängigkeit ins Projekt einbinden kann. Startpunkt
wäre also https://github.com/Jochen0x90h/coco
Würde mich mal interessieren was ihr davon haltet ;)
Jochen W. schrieb:> was ihr davon haltet
Nix.
uC sind event-driven.
Denk an Windows.
Events erfolgen durch Interrupts.
Da nicht jeder Interrupt voll abgearbeitet werden soll bevor der nächste
Interrupt kommen darf, macht eine Queue Sinn, Message-Warteschlange in
Windows.
Die loop (Arduino Jargon) nimmt dann den nächsten Auftrag aus der queue.
Das sollte das Prozessschema sein.
Ein Blinker wird also vom timer-interupt gesteuert. Nur das notigste:
Ok das Beispiel war noch zu einfach da die Blink-Funktion ja keinen
Zustand hat. Hier ist ein komplexeres Beispiel, ein VCP (virtueller
COM-Port über USB):
https://github.com/Jochen0x90h/coco-usb/blob/main/test/UsbSerialTest.cpp
Coroutine echo(Device &device, Buffer &buffer) {
while (true) {
// wait until USB device is connected
co_await device.untilReady();
while (device.ready()) {
// receive data from host
co_await buffer.read();
// send data back to host
co_await buffer.write();
}
}
}
Hier wartet die Coroutine bis USB verbunden ist, liest dann und schreibt
zurück. Das passiert nebenläufig z.B. neben der Behandlung des
Control-Endpoint. Besonders interessant für komplexere Protokolle wo man
auf die Antwort der Gegenstelle warten muss bevor es weitergeht.
(wie bekommt man Syntax Higlight hin? muss man [c] vor jede einzelne
Zeile schreiben?)
Jochen W. schrieb:> (wie bekommt man Syntax Higlight hin? muss man [ c] vor jede einzelne> Zeile schreiben?)
Nein. Vor die erste Zeile des Codeabschnitts, und nach der letzten kommt
ein [/c].
Dann sieht's so aus:
Komisch jetzt geht es auf einmal.
Jedenfalls ist die Idee von CoCo die Event Loop aus dem Beispiel von
Michael B. in eine Library zu verpacken und dann statt Funktionen
aufzurufen (LEDblinkt()) Coroutinen weiterlaufen zu lassen (resume()
wird auf dem Coroutine-Handle aufgerufen wobei das Handle vom Typ
std::coroutine_handle ist).
Jochen W. schrieb:> Würde mich mal interessieren was ihr davon haltet ;)
Ich denke, coroutinen sind deutlich Ressourcen sparender, als threading.
Ich muss geraden mit Zephyr arbeiten und wollte zum Debuggen alle
Optimierungen ausschalten. Danach brauchte ich nur noch einen halben
Tag, um für alle vom System gestarteten Threads die Stacks zu erhöhen,
damit die Software nicht gleich beim Start in einen stack overflow
läuft.
Es wird aber sicher noch Jahrhunderte dauern, bis die ersten Chip
Vendors den Vorteil erkennen werden ;-)
Ich finde Coroutinen auch sinnvoll. Allerdings würde ich es etwas
generischer machen, unabhängig von dem jeweils verwendeten uc.
Stattdessen ein I/F zur HW-Resourcen. Treiber für Timer, UART u.s.w.
kann dann jeder für sich schreiben und einbinden. Dann wäre es auf
"jedem" uc lauffähig. Oder habe ich da was übersehen? Ich hab es nur
kurz überflogen.
Warum das jetzt aus dem Projektordner in die Allgemeinheit geschoben
wurde, ist unklar.
Jochen W. schrieb:
> Würde mich mal interessieren was ihr davon haltet ;)
Wäre als Arduino-Library interessant.
Torsten R. schrieb:> Es wird aber sicher noch Jahrhunderte dauern, bis die ersten Chip> Vendors den Vorteil erkennen werden ;-)
Es gab auch schon mal Lisp-Maschinen
https://de.wikipedia.org/wiki/Lisp-Maschine
Darüber hinaus parallelisiert Haskell erstklassig, aus verschiedenen
Gründen. Schreibe ich nur deswegen, weil ich den Ansatz der "Coroutinen"
für C++ ganz gut finde - und denke, es hilft vielleicht, wenn man mehr
bei C++ bleibt bei der Betrachtung.
> Würde mich mal interessieren was ihr davon haltet ;)
Gaehn... machen wir schon seit 40 Jahren. Man bringt die Coroutine dann
halt dort unter wo's passt.
Das Text-LCD-Display beschreiben zB im 10ms Timer Tick, weil das Display
eh alles etwas langsamer, mit etwas Zeit dazwischen haben will. Also
jeden Tick einen Character schreiben. 3 Refreshes pro Sekunde ist etwa
das, was man einem Betrachter zumuten kann.
Eine Umwandlung eines Int oder Float Datentyps nach String fuer den
Display im Idle Loop. Ein Digit pro Durchgang. Nein, ein Printf() gibt's
nicht. Ist um Groessenordnungen zu klotzig.
Ein Regelungs routine im runtergeteilten Timer Tick. zB alle 100ms,
jeden 10. Timer Tick.
Die Messwerte kann man falls das Regelsystem das zulaesst im freerunning
ADC mit Kanalwechsel im Interrupt ansaugen. Allenfalls startet man den
Messzyklus auch mit dem Regel-Tick. Der Messzyklus kann auch mehrere
Samples auf den mehreren Kanaelen messen, und auch gleich die
Tiefpassung mit den Vorwerten machen.
usw. Alter Hut.
Ich benutze schon länger einen Scheduler, ursprünglich auf dem 80C51
entwickelt. Man kann ihm Callbacks übergeben, die dann nach der
angegebenen Zeit einmalig oder periodisch ausgeführt werden. Damit
lassen sich z.B. komplexe Ampelsteurungen programmieren, ohne daß es
unübersichtlich wird. Man stellt einen Callback rein und muß sich nicht
weiter darum kümmern. Callbacks können auch wieder gelöscht werden, z.B.
für Timeouts.
Der Aufruf erfolgt in der Mainloop über ein Timerflag. Damit umgeht man
Probleme mit mehreren Instanzen, wie bei Interrupts. Es ist also
erlaubt, daß Callbacks weitere Callbacks einstellen oder entfernen
können, ohne dafür Interrupts sperren zu müssen.
Die Callbacks werden in einer sortierten Liste angelegt. Somit hat man
nicht dutzende Zähler, sondern nur einen einzigen für den nächsten
Callback in der Liste. Periodische Callbacks sortieren sich einfach
wieder selber in die Liste ein.
Besonders vorteilhaft für µCs mit wenig RAM ist es, daß nur Platz für
die gleichzeitig aktiven Callbacks angelegt werden muß.
Eine Blink-LED trägt z.B. ein LED-Toggle als periodischen Callback ein.
Falls jemand wissen möchte, was coroutinen sind (Im wesentlichen, die
Möglichkeit, eine Funktion zu verlassen und sie an der verlassenen
Stelle, später weiter laufen zu lassen):
https://en.cppreference.com/w/cpp/language/coroutines
Ich find das ziemlich cool, man sieht nur leider mal wieder dass das
µc.net Forum schlichtweg nicht die richtige Anlaufstelle für solche
Projekte ist. Vermutlich verstehen die meisten hier deinen Code auch
nicht, anders kann ich mir nicht erklären weshalb gleich eine Hand voll
Antworten kommen die Coroutinen mit Timern vergleichen.
Ich hoffe nur du hast dich mit dem "Framework" drum herum nicht
übernommen. Eine sinnvolle Coroutine Library für Mikrocontroller zu
schreiben ist schon kompliziert genug, ohne dann auch noch zig Devices
mit Treibern usw. zu supporten. Als potenzieller Nutzer würde ich mir
hier mehr Dokumentation fürs "eigentliche Produkt" wünschen. Ich hab
C++20 Couroutines bisher nur grob überflogen und dann auf Grund der
enormen Komplexität gleich wieder beiseite gelegt. Ich hab jetzt das
README des eigentlichen 'coco' Repos gelesen und weiß nun weder ob die
Coroutinen Heap benötigen, noch hab ich ein schnelles Snippet gesehen
mit dem ich anfangen könnte.
Aber was nicht ist kann ja noch werden, sonst wie gesagt schon mal sehr
cool!
Vincent H. schrieb:> Vermutlich verstehen die meisten hier deinen Code auch> nicht ...
Ja, so geht es mir. Mit C++ habe ich so meine Probleme. Ich verliere
schnell den Überblick, wenn man tausende Dateien öffnen muß. Ehe man
sich bis zu den untersten Ebenen durchgekämpft hat, hat man längst
vergessen, was man eigentlich verstehen wollte.
Ich habe daher nur überblickt, was auf den ersten Ebenen steht. Zu den
eigentlichen Implementationen der Coroutinen bin ich noch nicht
durchgedrungen.
Peter D. schrieb:> ... Ich verliere> schnell den Überblick, wenn man tausende Dateien öffnen muß. Ehe man> sich bis zu den untersten Ebenen durchgekämpft hat, hat man längst> vergessen, was man eigentlich verstehen wollte.
Das ist total normal. Sobald die Probleme größer werden und die
komplette Lösung einfach nicht mehr in den Kopf passt, muss man etas
anders vorgehen. Dann muss man die Software an definierten Stellen in
Stücke schneiden und nur noch Teile der Software betrachten.
Wenn ich diese Schnittstelle dann gut beschreibe und die Implementierung
der Schnittstelle gut getestet habe, dann muss ich halt nicht mehr genau
verstehen, wie `schedule( task_a, next )` implementiert ist, wenn ich
mich darauf verlassen kann, das die Funktion das macht, was sie machen
soll.
Wo hält eine Coroutine eigentlich ihren Status? Einschlägige Dokus
sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für
(kleine) µC geeignet.
Bei Embedded hinken die Implementierungen der neueren Features oft
hinterher, und auch unterschiedlich bei verschiedenen Compilern. Noch
schwieriger wird es wenn es die Runtime Libs betrifft, ich musste schon
feststellen das ein aligned_alloc was es schon seit C11 gibt lange
Probleme machte.
Wie es sich mit dem Heap verhält kann man auf Systemen testen die einen
Memory Trace anbieten. Würde ich mal testen, habe aber gerade keine
Langeweile.
Johann L. schrieb:> Wo hält eine Coroutine eigentlich ihren Status? Einschlägige Dokus> sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für> (kleine) µC geeignet.
Meistens enthalten Coroutinen dann ja Endlosschleifen, also beenden sich
nie. Der Heap fragmentiert dann ja auch nicht bzw. man könnte den
operator new überladen und einfach fortlaufend Speicher rausgeben den
man nicht wieder freigeben kann. Nach der Initialisierung ändert sich
der Speicherfüllstand dann auch nicht mehr. Vielleicht kann man mit dem
[ [noreturn]] Attribut an der aufrufenden Funktion dafür sorgen, dass
die Coroutine auf dem Stack der aufrufenden Funktion angelegt wird.
Müsste man mal genauer untersuchen.
Vincent H. schrieb:> Aber was nicht ist kann ja noch werden, sonst wie gesagt schon mal sehr> cool!
Klar ist alles viel mehr geworden als ursprünglich gedacht. Einstieg ist
aber
https://github.com/Jochen0x90h/coco-loop/blob/main/test/LoopTest.cpp wo
man mit dem Prinzip experimentieren kann oder
https://github.com/Jochen0x90h/coco-uart/blob/main/test/UartSendTest.cpp
für Spaß mit der UART. Die "Treiber" setzen in separaten Libraries auf
coco-loop (Event Loop) auf, paar sind schon vorhanden oder kann man sich
selber schreiben. Ich selbst nutze es inzwischen für das erste Projekt
auf Arbeit, daher supporte ich zumindest das was ich gerade brauche und
vielleicht finden sich ja Mitstreiter die es auch nutzen oder sich
inspirieren lassen und was eigenes machen
Jochen W. schrieb:> Johann L. schrieb:>> Wo hält eine Coroutine eigentlich ihren Status? Einschlägige Dokus>> sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für>> (kleine) µC geeignet.>> Meistens enthalten Coroutinen dann ja Endlosschleifen,> also beenden sich nie.
Sie verlassen aber ihren Kontext und kehren wieder dahin zurück. Der
Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem
Stack.
Johann L. schrieb:> Sie verlassen aber ihren Kontext und kehren wieder dahin zurück. Der> Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem> Stack.
Der komplette Stack GEHÖRT zum Kontext, zusätzlich zu den Registern.
Ein irrer Speicherverbrauch, elegant versteckt hinter einfachen
Funktionen.
Ein Grund, warum aktuelle Software so gross und langsam ist.
Wer unbedingt wartende Routinen schreiben will weil er das irgendwie für
einfacher hält
1
voidreceiveserial(void)
2
{
3
co_awaitdevice.untilReady();
4
b1=device.read();
5
co_awaitdevice.untilReady();
6
b2=device.read();
7
}
kann das im kooperativen Multitasking
1
voidreceiveserial(void)
2
{
3
while(device.notready())yield();
4
b1=device.read();
5
while(device.notready())yield();
6
b2=device.read();
7
}
8
voidyield(void)
9
{
10
// system queue message Bearbeitung, darunter:
11
if(queue.size>0)switch(queue[0].msg)
12
{
13
caseM_processstupidly:
14
receiveserial();
15
break;
16
default:
17
// alle anderen sinnvollen messages
18
}
19
}
20
voidloop(void)// siehe mein erster Beitrag
21
{
22
yield();
23
}
und sieht gleich sehr gut, wie yield rekursiv aufgerufen wird und der
ganze stack erhalten bleibt.
Macht man so spätestens seit Windows 1.0, aber selten weil es meist
bessere Lösungen gibt wie state machines.
Ich hab mir deine Lib gerade mal auf github angesehen. Du hast da eine
Menge Arbeit geleistet aber ich denke dass das ehr nichts für die
Allgemeinheit ist. Das liegt sicher einmal an der Masse des Codes, der
Komplexität die viele überfordern wird und an der Verlässlichkeit, dass
das Projekt auch noch in 5 Jahren existiert. Die Coroutinen an sich sind
sicher nicht schlecht und ich ziehe diese Art der Programmierung einem
rtos vor (wenn möglich). Im letzten Jahrtausend hatte Adam Dunkels mit
seinen Protothreads einen ähnlichen Ansatz in reinem C veröffentlicht.
So was ähnliches in C++ verwende ich auch hin und wieder.
Ganz eingetaucht bin ich nicht in deinen Code, das ist mir zu
umfangreich. Im realen Leben schlagen sich viele ja auch mit noch einer
Menge anderer Libs herum die sie nicht so einfach nach oder umentwickeln
wollen oder können. Und immer häufiger sind die Beispiele für die
allermeisten Sachen die über uart, spi oder i2c angebunden sind nur noch
als Arduino-Libs verfügbar. Da passt dann eine neue Gesamt-Lib für alles
nicht mehr dazu. Solche großen Libs die als Unterbau für alles genutzt
werden können haben es sowieso sehr schwer. Ich denke das nur am mbed.
Die Leute haben da sicher vielen guten Code geschrieben, schade dass das
nun irgendwie alles für die Tonne ist.
Michael B. schrieb:> Nix.
Muss ich leider auch teilweise zustimmen. Grob gesagt (aber bitte nicht
Wort wörtlich verstehen): Multitasking braucht man nur dann, wenn man
selber nicht programmieren kann. Theoretisch, kann man alles in kleinen
Stücke aufteilen. Der uC wird nicht all zu lange blockiert. Es reicht
eine while schleife mit einfache Funktionsaufrufen aus. Das klappt in
der Regel ganz gut, wenn man Funktionen mit delayms oder delayus
vermeidet und anstelle dann State Machines programmiert. Bzw Polling
nimmt anstelle auf etwas zu warten.
Allerdings gibt es Use-Cases wo Multitasking doch Vorteile hat. Zum
Beispiel wenn man Zeitlich bestimme Events sonst nicht schnell genug
bearbeiten kann. (Ja man kann darüber diskutieren wieso soetwas nicht in
ein interrupt landet). Oder anderes Beispiel, wenn man nicht
programmieren kann. Ich würde behaupten, wenn man alleine auf ein uC
programmiert, dann kommt man relativ weit auch ohne Multitasking.
Wenn wir dann noch weitergehen, und nicht nur die Zeitliche beachten,
sondern auch Speicherschutz. Dann kann man auch Tasks relativ gut
voneinander schützten. Separate Stack, Memory Protection. Dann noch ein
kleiner schritt und man ist bei VMs angekommen :)
Andras H. schrieb:> Muss ich leider auch teilweise zustimmen. Grob gesagt (aber bitte nicht> Wort wörtlich verstehen): Multitasking braucht man nur dann, wenn man> selber nicht programmieren kann.
Sehr arrogant und kurzsichtig, da hast du es sicher noch nicht mit
komplexen Embedded Systemen zu tun gemacht. Embedded sind nicht nur
Blinkys die noch ein paar Tasten verarbeiten müssen.
Wenn mir jemand so etwas in einem Vorstellungsgespräch erzählt ist das
sehr schnell zu Ende.
Jürgen schrieb:> Ich hab mir deine Lib gerade mal auf github angesehen. Du hast da eine> Menge Arbeit geleistet aber ich denke dass das ehr nichts für die> Allgemeinheit ist. Das liegt sicher einmal an der Masse des Codes, der> Komplexität die viele überfordern wird und an der Verlässlichkeit, dass> das Projekt auch noch in 5 Jahren existiert.
Klar da ist natürlich was dran daher stelle ich das ja mal der
öffentlichen Diskussion und sollten welche mitmachen wollen dann erhöht
das ja die Chance, dass es in 5 Jahren noch existiert. Mit Arduino ist
auch richtig, es wurde ja auch schon vorgeschlagen das als Arduino-Libs
zu verpacken. Das wäre dann eine andere "Darreichungsform", müsste mir
mal ansehen ob das geht (bzw. wäre super wenn jemand, der viel
Arduino-Erfahrung hat, das ansehen würde).
Johann L. schrieb:> Jochen W. schrieb:>> Johann L. schrieb:>>> Wo hält eine Coroutine eigentlich ihren Status? Einschlägige Dokus>>> sagen "Heap", und damit wären C++ Coroutinen nicht wirklich gut für>>> (kleine) µC geeignet.>>>> Meistens enthalten Coroutinen dann ja Endlosschleifen,>> also beenden sich nie.>> Sie verlassen aber ihren Kontext und kehren wieder dahin zurück. Der> Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem> Stack.
Ich meine wenn der Kontext auf dem Heap ist, man aber nur in der
Initialisierungsphase ein paar Coroutinen startet, die Endlosschleifen
enthalten, dann passieren zur Laufzeit keine Allokationen mehr und es
kann keinen "out of Memory" geben.
Kooperativ und Präemptiv haben beide Vor- und Nachteile, das sollte
einem schon bewusst sein.
Präemptiv mit Tasks/Threads braucht eben mehr Speicher durch die eigenen
Stacks je Thread und kostet Rechenzeit für die Kontextwechsel, dafür
kann man Threads einrichten die wichtige Dinge mit Vorrang ausführen.
Bei Kooperativ darf keine Funktion blockieren, das würde die anderen
stören. Trotzdem kann man das gut skalieren und es hat weniger Overhead
wenn die Funktionen über Ereignisse ausgelöst werden.
Und man kann auch beide kombinieren wenn es das System hergibt.
Mit CoRoutinen vermeidet man lange unübersichtliche Dispatcher, ich
finde die schon interessant. Sicherlich kommt man auf kleinen Systemen
auch ohne aus, aber das Problem fängt immer dann an wenn der
Funktionsumfang wächst und immer mehr angebaut wird. In der c't gab es
mal ein schönes Bild, im meine zu Windows98, wo ein schöner Palast
sichtbar war, der aber auf einem fragilen Fundament stand. Da ist eine
solide Basis besser wenn man etwas erweitern möchte.
Jochen W. schrieb:> es wurde ja auch schon vorgeschlagen das als Arduino-Libs> zu verpacken
Ich will nicht gerade sagen dass ich Arduino liebe. Der code vom core
ist aber aus meiner Sicht recht gut und durchdacht. Das was da drunter
die Hardware abstrahiert gefällt mir (z.B. für STM32) auch nicht. Da
geht's mir wie dir und ich bediene die Register lieber direkt ohne die
Libs von ST. Selten benutze ich ESPxxx und wenn mit Arduino. Das ist mir
da lieber als das Expressif SDK direkt. Wenn es in dem Umfeld was nützen
soll, dann reicht eine absolut abgespeckte Version die nur die
coroutinen beinhaltet und sich mit dem normalen i2s,spi,uart u.s.w.
benutzen lässt.
Andras H. schrieb:> Multitasking braucht man nur dann, wenn man> selber nicht programmieren kann.
Das kann so evt. für sehr kleine Systeme gelten. Allgemein natürlich
nicht.
Es wäre absurd, sich bei jeder Multitasking Anwendung erst mal selbst
noch ein Task-System zu programmieren.
Wer aber natürlich meint, er brauche auf einem 8 Bit Controller für ein
LED Blinken und eine Taster-Abfrage ein fertiges RTOS mit Multitasking,
der sollte in der Tat an seinen Fähigkeiten arbeiten.
Michael B. schrieb:> Johann L. schrieb:>> Sie verlassen aber ihren Kontext und kehren wieder dahin zurück. Der>> Kontext muss also irgendwo gespeichert werden, und zwar nicht auf dem>> Stack.>> Der komplette Stack GEHÖRT zum Kontext, zusätzlich zu den Registern.>> Ein irrer Speicherverbrauch, elegant versteckt hinter einfachen> Funktionen.>> Ein Grund, warum aktuelle Software so gross und langsam ist.
Das hat hier nichts mit groß und langsam zu tun. Coroutinen sind nicht
langsam. Sie sind genauso schnell wie "normale" Funktionsaufrufe. Das
ist ja genau deren Vorteil.
Mit deinem yield() Vorschlag versteckst du auch nur Funktionsaufrufe.
Das ist auf keinen Fall besser wie Coroutinen.
Was man nicht aus dem Bick verlieren darf. Die C++ Entwicklung wird
nicht primär für µC gemacht.
Jürgen schrieb:> Wenn es in dem Umfeld was nützen> soll, dann reicht eine absolut abgespeckte Version die nur die> coroutinen beinhaltet und sich mit dem normalen i2s,spi,uart u.s.w.> benutzen lässt.
Vielleicht kam es nicht richtig rüber aber die abgespeckte Version gibt
es schon, das wäre dann nur coco https://github.com/Jochen0x90h/coco
Das enthält das Build-System (CMake + conan), die Coroutinen, die
Queue-Klasse (IntrusiveMpscQueue.hpp), die Umschaltung der von den
Herstellern bereitgestellten Header (#include
<coco/platform/platform.hpp> um z.B. stm32c031xx.h zu erhalten wenn man
den uC im Build-System ausgewählt hat) und noch HAL-artige
Hilfsfunktionen und Klassen (die man noch auslagern könnte aber erstmal
nicht stören da nur Header).
Wer nur die Event-Loop haben will nimmt coco-loop und baut sich alle
Treiber selber.
Veit D. schrieb:> Das hat hier nichts mit groß und langsam zu tun. Coroutinen sind nicht> langsam. Sie sind genauso schnell wie "normale" Funktionsaufrufe. Das> ist ja genau deren Vorteil.> Mit deinem yield() Vorschlag versteckst du auch nur Funktionsaufrufe.> Das ist auf keinen Fall besser wie Coroutinen.
Doch, weil der Programmierer noch weiss, was für einen Unsinn in der Not
er treibt.
Bei Coroutinen ist der Unsinn gut versteckt und er glaubt an die beste
Erfindung seit dem Rad die er möglichst immer überall verwenden sollte.
Die Coroutine ist zwar nicht merklich langsamer als yield, aber
speicherintensiv für JEDE hängende Routine. Das können schnell einige
sein, wenn der Programmiere den Programmierstil geil findet.
Bei yield weiss er, dass er das nur in der Not verwenden sollte, da
hängt das Programm höchstens in einer Routine.
Hallo,
das mag im Groben und Ganzen fast stimmen. Aber so wie du es sagst, so
abwertend, so stimmt es einfach nicht.
> Die Coroutine ist zwar nicht merklich langsamer als yield, aber> speicherintensiv für JEDE hängende Routine.
Coroutinen sind nicht langsamer als yield(). Können sie auch gar nicht
sein. Weil man in yield() auch nur Funktionen aufruft. Coroutinen sind
auch nur Funktionsaufrufe. Die Links hatte ich nicht umsonst gezeigt.
Diskutiere sachlich richtig nach besten Wissen und Gewissen.
Michael B. schrieb:> und sieht gleich sehr gut, wie yield rekursiv aufgerufen wird und der> ganze stack erhalten bleibt.
Probier mal Dein Beispiel mit receiveserial1() und receiveserial2(),
also man bearbeitet 2 serielle Schnittstellen parallel. Dann sieht man
gleich, dass das gar nicht funktioniert weil z.B. receiveserial1()
yield() aufruft aber wenn von da aus receiveserial2() gestartet wird
dann bleibt receiveserial1() hängen oder wird rekursiv nochmal
aufgerufen, also fängt nochmal von vorne an. Das müsste schnell einen
Stack Overflow geben.
Jochen W. schrieb:> Probier mal Dein Beispiel mit receiveserial1() und receiveserial2(),> also man bearbeitet 2 serielle Schnittstellen parallel. Dann sieht man> gleich, dass das gar nicht funktioniert
Ja, daher nur ein yield zu einer Zeit.
Für mich ist das auch Neuland ;-)
Diesen (englischen) Vortrag fand ich gut:
https://www.youtube.com/watch?v=kIPzED3VD3w
Jetzt frage ich mich :
Schleppe ich mir damit automatisch dynamische Speicherverwaltung und
Exceptions in mein embedded System oder geht es auch irgendwie statisch
?
Also Coroutinen werden einmalig angelegt und löschen sich nicht selbst,
bleiben im Speicher und können, wenn benötigt, einfach zyklisch wieder
aktiviert werden ?
Hans-Georg L. schrieb:> Jetzt frage ich mich :> Schleppe ich mir damit automatisch dynamische Speicherverwaltung und> Exceptions in mein embedded System oder geht es auch irgendwie statisch ?
Exceptions sind eine andere Nummer und haben mit Coroutinen nichts zu
tun. Benutze ich momentan nicht.
> Also Coroutinen werden einmalig angelegt und löschen sich nicht selbst,> bleiben im Speicher und können, wenn benötigt, einfach zyklisch wieder> aktiviert werden ?
Ja, wenn sie einmalig auf dem Heap angelegt werden dann bleiben sie da
und es findet keine weitere dynamische Speicherverwaltung statt solange
man nicht als Reaktion auf ein Ereignis eine neue Coroutine startet.
Angeblich kann der Compiler sogar die dynamischen Allokationen unter
bestimmten Bedingungen wegoptimieren aber das müsste man mit dem
Compiler Explorer mal genauer untersuchen.
wenn ich mir das hier durchlese:
https://en.cppreference.com/w/cpp/language/coroutinesJochen W. schrieb:> Hans-Georg L. schrieb:>> Jetzt frage ich mich :>> Schleppe ich mir damit automatisch dynamische Speicherverwaltung und>> Exceptions in mein embedded System oder geht es auch irgendwie statisch ?>> Exceptions sind eine andere Nummer und haben mit Coroutinen nichts zu> tun. Benutze ich momentan nicht.>
Each coroutine is associated with the promise object, manipulated from
inside the coroutine. The coroutine submits its result or exception
through this object. Promise objects are in no way related to
std::promise.
Deshalb meine Vermutung das ich evtl. Exceptions mit der Lib
einschleppe.
>> Also Coroutinen werden einmalig angelegt und löschen sich nicht selbst,>> bleiben im Speicher und können, wenn benötigt, einfach zyklisch wieder>> aktiviert werden ?>> Ja, wenn sie einmalig auf dem Heap angelegt werden dann bleiben sie da> und es findet keine weitere dynamische Speicherverwaltung statt solange> man nicht als Reaktion auf ein Ereignis eine neue Coroutine startet.
When a coroutine reaches a suspension point
the return object obtained earlier is returned to the caller/resumer,
after implicit conversion to the return type of the coroutine, if
necessary.
When a coroutine reaches the co_return statement, it performs the
following:
calls promise.return_void() for co_return; co_return expr; where expr
has type void
or calls promise.return_value(expr) for co_return expr; where expr has
non-void type
destroys all variables with automatic storage duration in reverse order
they were created.
calls promise.final_suspend() and co_awaits the result.
Falling off the end of the coroutine is equivalent to co_return;, except
that the behavior is undefined if no declarations of return_void can be
found in the scope of Promise. A function with none of the defining
keywords in its function body is not a coroutine, regardless of its
return type, and falling off the end results in undefined behavior if
the return type is not (possibly cv-qualified) void.
When the coroutine state is destroyed either because it terminated via
co_return or uncaught exception, or because it was destroyed via its
handle, it does the following:
calls the destructor of the promiseobject.
calls the destructors of the function parameter copies.
calls operator delete to free the memory used by the coroutine state.
transfers execution back to the caller/resumer.
Also darf man nie co_return aufrufen. ?
Wie bekommt man dann aber das Resultat zurück ?
> Angeblich kann der Compiler sogar die dynamischen Allokationen unter> bestimmten Bedingungen wegoptimieren aber das müsste man mit dem> Compiler Explorer mal genauer untersuchen.
The call to operator new can be optimized out (even if custom allocator
is used) if The lifetime of the coroutine state is strictly nested
within the lifetime of the caller, and the size of coroutine frame is
known at the call site. In that case, coroutine state is embedded in the
caller's stack frame (if the caller is an ordinary function) or
coroutine state (if the caller is a coroutine).
Dann wären die Daten wieder auf dem Stack ?
Vielleicht könnte man den Compiler mit "placement new" überlisten.
Ich will aber nicht weiter herum meckern ;-)
Eine schöne Library von dir Daumen hoch !
Hans-Georg L. schrieb:> Also darf man nie co_return aufrufen. ?> Wie bekommt man dann aber das Resultat zurück ?
Ja um new/delete zu vermeiden sollte co_return nie aufgerufen werden,
also die Coroutine ewig "leben". Coroutinen sind also weniger da um ein
Resultat zurückzugeben sondern sind Prozesse, die auf etwas warten und
dann etwas tun und das in einer Endlosschleife.
Beispielsweise habe ich einen virtuellen COM-Port wo die LED für 100ms
leuchten soll sobald man was sendet. Das mache ich mit einer Coroutine
wie folgt:
1
Barrier<>sendLedBarrier;
2
3
CoroutinesendLed(Loop&loop){
4
while(true){
5
co_awaitsendLedBarrier.untilResumed();
6
debug::setRed(true);
7
co_awaitloop.sleep(100ms);
8
debug::setRed(false);
9
}
10
}
Die Coroutine wird gestartet und wartet an der Barrier, das ist wie eine
Eisenbahnschranke wo man erstmal warten muss. Wenn Daten gesendet werden
dann dürfen alle, die an der Schranke warten, weiterlaufen (in dem Fall
wartet nur die eine Coroutine):
Beide Coroutinen beenden sich nie, daher ist das mit dem new/delete auch
kein Problem.
Um noch einen Double-Buffer zu erzeugen kann man die send Coroutine
einfach zwei mal starten, dann empfängt immer mindestens einer Daten
während der andere sendet. Genauso mit receive.
Ich finde es löblich dass du dich für modernes C++ auf kleinen
Mikrocontrollern entschieden hast.
Coroutinen fand ich immer akademisch interessant, in der Praxis finde
ich aber Multithreading auf einem Mikrocontroller schwer zu debuggen.
Ein Kollege der einen Fetisch für Coroutinen hat, hat diese ähnlich wie
du implementiert, auch mit C++ und wir haben das ein Weilchen in einem
Projekt benutzt. Das war auch wirklich schön und wie aus dem Lehrbuch,
wurde aber von den anderen Kollegen rundweg abgelehnt und schließlich
durch eine primitive Superloop ersetzt. Schade.
Ich persönlich mag ereignisgetriebene Single Stack Systeme in
Kombination mit Zustandsautomaten am liebsten und verwende die in der
Praxis am häufigsten.