Es gibt Programmkonstrukte, die Daten produzieren. Es gibt
Programmkonstrukte, die Daten holen. Bas können bytes sein, oder
sonstige Objekte / Nachrichten. Was mir aber vor einer Weile aufgefallen
ist, die Ansätze passen nicht besonders gut zusammen:
1) pulling
1
getData(){
2
producer.getData()
3
processdata
4
returnprocesseddata
5
}
2) pushing
1
processData(data){
2
processdata
3
consumer.processData(processed)
4
}
Der stack kann da auch eventuell recht lang werden, wenn da viele drinn
hängen. Es gibt auch unterschiedliche vor / nachteile. getData holt man
sich nur Daten, wenn man sie auch braucht. Aber man weiss nicht, ob es
neue gäbe.
Das Problem ist aber, unter anderem, die zusammenzuhängen. Es gibt da ja
diverse Ansätze, sowas zu überbrücken. Die eine Richtung ist ja recht
simpel, consumer.processData(producer.getData()). Wobei dass dan ja
blockieren kann.
Bei der anderen Richtung ist es komplizierter. z.B. kann man eine Queue
nehmen.
1
consumer.processData=queue.push;
2
producer.getData=queue.pull;
3
...
4
while(data){
5
processData(data);// Wenn neue daten kommen, ruft eventuell consumer.processData und dann queue.push auf
6
if(queue.has_data)
7
resultat=getData()// ruft eventuell producer.getData und dann queue.pull auf
8
}
Wobei man dann nur ein getData in der Chain haben darf, was ja nicht
immer gut ist.
Andere damit in zusammenhang stehende Konzepte, mit dem man das besser
lösen kann, sind coroutinen / generatoren oder auch ganze Threads oder
Prozesse. z.B. In bash arbeitet man mit Pipes und Prozessen.
1
a|b|c
Die pipes sind quasi queues, und das vorhergehende Programm wird einfach
angehalten, wenn die voll ist, bis das nächste liest. JS hat auch
Readable und Writable stream Klassen, und ein backpressure konzept.
In C Programmen scheint man nur selten solche Pipelines / Streams zu
haben, und man kommt eigentlich normalerweise recht gut zurecht, wenn
man sich dann halt selbst drum kümmert, wenn man doch mal eine Queue
braucht oder so. Es gibt auch Programme, die eine Pipeline für
spezifische Sachen zusammenbauen. z.B. für Sound, oder bei gstreamer
auch für Videodaten.
Aber wollte ich ein grösseres Programm in C machen, und mehrere Teile
modular pipelinemässig aneinander hängen können, gibt es da was, um das
alles einfacher unter einen Hut zu bringen? Irgend was für beliebige
Datentypen, was simples, mit dem man producer, transformer, consumer,
etc. aneinanderhängen kann, eventuell die streams auch aufsplitten,
zusammenführen, usw. Etwas mit geringem overhead, aber auch so das die
die es nicht mehr braucht einfach wieder aufräumen kann? Gibt es da für
C irgend ein allgemeines Pattern, oder eine Library oder so, mit dem man
alle Fälle erschlagen kann? Eine Abstraktion, die da alle fälle abdeckt?
Da für jeden Fall das manuell zu machen, skaliert nicht wirklich, wenn
das Projekt gross & dynamisch werden soll. Und ich kann mir schon ein
allgemeines interface ausdenken, das beide fälle abdeckt, aber wenn ich
das mache, dann weder simpel, noch besonders effizient (der letzte
Versuch war ein mein loop und mehrere Callbacks in Structs als
Interfaces zur signalisierung, wenn Daten da sind, wann sie konsumiert
wurden, usw. Hat zwar funktioniert, war aber langsam und da hätte keiner
mehr durchgeblickt...). Das muss doch irgendwie einfacher gehen...
Super gute Frage, die ich mir auch schon häufiger gestellt habe, aber
primär im Kontext von Eingebetteten Systemen.
Ich habe keine richtige Antwort auf deine Frage - leider. Daher hänge
ich mich hier dran. Was ich dir aber grob sagen kann ist wie wir es
machen (in µC, aber sowas kann man natürlich auch auf dem PC machen):
Jedes Modul, damit sind explizit auch consumer (z.B. ein
Signalprocessing Modul) und producer (z.B. ein Sensor Controller)
gemeint, hat eine eigene C-file + header. Jedes Modul hat eine eigene
state machine, die ohne jegliche Kondition von einem zentralen
Controller aufgerufen wird. Das ist im µC einfach in der main loop, aber
afaik macht z.B. Qt das auf dem PC ähnlich.
Jedes Modul hat einen eigenen Fifo für Daten und eine API, um ebenjene
aus abholen zu lassen. Wir pollen grundsätzlich, wenn es um Verbindungen
zwischen den Modulen geht. Erst wenn es neue Daten gibt, springt die
state machine einen Zustand weiter und holt die Daten über die
entsprechende Funktion des anderen Moduls. Optional werden die Daten
dann wieder in einen Fifo gepackt, oder direkt, bzw im nächsten
Durchlauf der state machine verarbeitet.
D.h. es gibt keine modulübergreifende Queue oder messaging Modul.
Das heißt aber, dass es nur eine Senke pro Datum geben kann, denn danach
ist es weg ;)
Nachteile sind auch hier: Es ist nicht besonders übersichtlich, wie die
Zusammenhänge sind und die modulinternen FSM können recht komplex
werden. Ist also sicherlich keine allgemeingültige Lösung.
Vermutlich ist der Ansatz zu simpel für dich und mit Sicherheit nicht
für Audio etc. geeignet.
Bin sehr auf weitere Antworten gespannt.
🐧 DPA 🐧 schrieb:> Die pipes sind quasi queues, und das vorhergehende Programm wird einfach> angehalten, wenn die voll ist, bis das nächste liest. JS hat auch> Readable und Writable stream Klassen, und ein backpressure konzept.
Pipes sind synchronisierte Queues mit Nachrichtengröße 1 Byte.
Du kannst Dir doch einfach eine synchronisierte Queue als Template
schreiben (macht man mit mutex und condition-variable).
Oder Du nimmst POSIX-Message-Queues oder XSI-MessageQueues (ggf. mit
C++-Wrapper).
Wilhelm M. schrieb:> Pipes sind synchronisierte Queues mit Nachrichtengröße 1 Byte.
Nein, in eine Pipe passt viel mehr rein. Einfach mal selbst probieren:
1
#!/bin/sh
2
(
3
echo'Eine pipe kann schon einiges halten!'
4
echo Hier wurde noch nichts angehalten >&2
5
) | (
6
sleep 2
7
echo In der pipe war das hier gespeichert: "$(cat)"
8
)
Wäre sonst ja extrem ineffizient...
> Du kannst Dir doch einfach eine synchronisierte Queue als Template> schreiben (macht man mit mutex und condition-variable).
Template wäre dann aber C++, nicht C. Und wenn du von mutex redest, hast
du wohl Threads im sinn. Aber wenn man da viele Threads zum Verarbeiten
der Daten zusammensetzt, wird das auch Schnell ineffizient.
Jan K. schrieb:> Jedes Modul hat einen eigenen Fifo für Daten und eine API, um ebenjene> aus abholen zu lassen. Wir pollen grundsätzlich, wenn es um Verbindungen> zwischen den Modulen geht. Erst wenn es neue Daten gibt, springt die> state machine einen Zustand weiter und holt die Daten über die> entsprechende Funktion des anderen Moduls. Optional werden die Daten> dann wieder in einen Fifo gepackt, oder direkt, bzw im nächsten> Durchlauf der state machine verarbeitet.
Das tönt interressant. Den Ansatz muss ich mir mal etwas genauer
ansehen.
🐧 DPA 🐧 schrieb:> Wilhelm M. schrieb:>> Pipes sind synchronisierte Queues mit Nachrichtengröße 1 Byte.>> Nein, in eine Pipe passt viel mehr rein. Einfach mal selbst probieren:
Nein, eine Nachricht ist das, was atomar versendet / empfangen werden
kann. Und das ist bei der Pipe eben 1 Byte.
Natürlich passen mehrere Nachrichten (Bytes) in den Warteraum (mind.
PIPE_BUF).
>> Du kannst Dir doch einfach eine synchronisierte Queue als Template>> schreiben (macht man mit mutex und condition-variable).>> Template wäre dann aber C++, nicht C. Und wenn du von mutex redest, hast> du wohl Threads im sinn.
Sicher, mehrere abhängige blockierende Aufrufe in einem Thread ist keine
gute Idee ;-)
> Aber wenn man da viele Threads zum Verarbeiten> der Daten zusammensetzt, wird das auch Schnell ineffizient.
Kommt drauf an ... (s.a. thundering herd problem).
🐧 DPA 🐧 schrieb:> Aber wollte ich ein grösseres Programm in C machen, und mehrere Teile> modular pipelinemässig aneinander hängen können, gibt es da was, um das> alles einfacher unter einen Hut zu bringen?
Klares NEIN. Und nicht nur in C.
Es gibt Unzählige von Anwendungen und Frameworks, die versucht haben,
solche "Filternetzwerke" mit einem einfach benutzbaren, einheitliche
Modell zu behandeln.
Das Fazit ist: Es hat bei KEINEM wirklich umfassend gut funktioniert.
Und die waren meistens in irgendeiner OO-Sprache implementiert
(überwiegend C++), was einiges vereinfacht. In C wird es nur noch
schwieriger...
Rolf M. schrieb:> 🐧 DPA 🐧 schrieb:>> Das Problem ist aber, unter anderem, die zusammenzuhängen.>> https://en.wikipedia.org/wiki/Coroutine#Subroutines
Coroutinen in reinem C zu implementieren ist natürlich sehr hässlich.
Man kann aber Coroutine mit einer FSM simulieren, wobei wir dann wieder
bei dem schon genannten Ansatz wären, Protokollparser (FSM) zu
schreiben.
In C++ (20) ist das natürlich "etwas" eleganter, da Coroutinen in der
Sprache eingebaut sind. In dem Fall muss man sich dann ein Waiter
schreiben. Die stdlib bietet da aber derzeit auch nur wenig
Unterstützung.
Zudem ist eine Heap-Allokation für das Zustandsobjekt notwendig. Auf
einem µC bietet es sich an, dann dafür einen Klassenspezifischen
new-Operator zu schreiben (der dann vom Compiler verwendet wird), der
auf einem static Pool arbeitet (so bekommt man auch C++Coroutinen zum
Laufen, auch wenn man keinen Heap benutzen will oder kann, etwa bei
kleinen B-Bittern).
🐧 DPA 🐧 schrieb:> Aber wollte ich ein grösseres Programm in C machen, und mehrere Teile> modular pipelinemässig aneinander hängen können, gibt es da was, um das> alles einfacher unter einen Hut zu bringen? Irgend was für beliebige> Datentypen, was simples, mit dem man producer, transformer, consumer,> etc. aneinanderhängen kann, eventuell die streams auch aufsplitten,> zusammenführen, usw. Etwas mit geringem overhead, aber auch so das die> die es nicht mehr braucht einfach wieder aufräumen kann? Gibt es da für> C irgend ein allgemeines Pattern, oder eine Library oder so, mit dem man> alle Fälle erschlagen kann? Eine Abstraktion, die da alle fälle abdeckt?
Was spricht gegen pipe / CreatePipe?
Ansonsten verwenden die meisten grossen CAD Programme nach wie vor
einfache Files zum Datenaustausch.
Sonst fällt mir noch COM unter Windows ein, das ist wirklich gut, weil
es unabhängig von der Sprache und com Compiler ist.
Oder halt D-Bus/CORBA oder OpenMP.
Deine Anforderungen sind aber viel zu vage, um da was konkretes sagen zu
können....
Wilhelm M. schrieb:> Rolf M. schrieb:>> 🐧 DPA 🐧 schrieb:>>> Das Problem ist aber, unter anderem, die zusammenzuhängen.>>>> https://en.wikipedia.org/wiki/Coroutine#Subroutines>> Coroutinen in reinem C zu implementieren ist natürlich sehr hässlich.
Das muss es nicht sein. Es gibt diverse Libraries, die das Abstrahieren
(einen eigenen Stack erstellen, dort hin switchen). z.B. POSIX
swapcontext (wurde zwar wieder aus POSIX entfernt, ist aber noch in der
glibc drin). Dann das Equivalent von GNU pth: pth_uctx_switch. Und es
gibt noch diverse andere libraries.
Wobei das natürlich viel Speicher verschwendet, immer einen ganzen Stack
anlegen...
Wilhelm M. schrieb:> In C++ (20) ist das natürlich "etwas" eleganter
Naja, da hat man dafür wieder die Limitation mit der Unterscheidung
zwischen normalen Funktionen und diesen Funktionen. Die haben ja keinen
Stack, sondern in ein fixes Objekt mit den lokalen Variablen der
Funktion. Das wird platzsparender und effizienter sein, aber man wird
vermutlich nicht in Unterfunktionen / Callbacks zwischen denen switchen
können, nehme ich an?
Wie auch immer, ich sollte da mal wieder etwas damit
herumexperimentieren.
🐧 DPA 🐧 schrieb:> Wilhelm M. schrieb:>> Rolf M. schrieb:>>> 🐧 DPA 🐧 schrieb:>>>> Das Problem ist aber, unter anderem, die zusammenzuhängen.>>>>>> https://en.wikipedia.org/wiki/Coroutine#Subroutines>>>> Coroutinen in reinem C zu implementieren ist natürlich sehr hässlich.>> Das muss es nicht sein. Es gibt diverse Libraries, die das Abstrahieren> (einen eigenen Stack erstellen, dort hin switchen).
Wenn Du die nutzen kannst für Dich: ja.
> z.B. POSIX> swapcontext (wurde zwar wieder aus POSIX entfernt, ist aber noch in der> glibc drin). Dann das Equivalent von GNU pth: pth_uctx_switch.
Würde ich nicht machen, damit wirst Du nicht glücklich.
Dann kannst Du auch gleich statt der Coroutine eine FSM in einem eigenen
Thread laufen lassen, die eine Coroutine simuliert. Das ist
state-of-the-art. Natürlich kann C keine richtigen Coroutinen und man
hat nicht die Schlüsselworte co_await, co_yield und co_return.
> Und es> gibt noch diverse andere libraries.
Sicher ;-)
> Wilhelm M. schrieb:>> In C++ (20) ist das natürlich "etwas" eleganter>> Naja, da hat man dafür wieder die Limitation mit der Unterscheidung> zwischen normalen Funktionen und diesen Funktionen.
Wo soll da eine "Begrenzung" sein: man gewinnt ja gerade das
coroutine-Konzept hinzu!
> Die haben ja keinen> Stack, sondern in ein fixes Objekt mit den lokalen Variablen der> Funktion. Das wird platzsparender und effizienter sein, aber man wird> vermutlich nicht in Unterfunktionen / Callbacks zwischen denen switchen> können, nehme ich an?
Was soll das denn werden? Was willst Du denn da zusammenhacken?
Mach Dir erstmal Deine Anforderungen klar, die kann man nämlich aus
Deinem schwurbeligen Post nicht wirklich entnehmen. Ich vermute, dass Du
das alles gar nicht brauchst.
Teile Dein SW-Artefakt vernünftig in unabhängige Teile auf, lass die
durch eigene Threads abarbeiten und wähle eine (speicherbasierte)
Kommunikation Deiner Wahl: synchronisierte Queues, Bäume, Listen,
Vectoren, ... Alternativ wähle nachrichtenbasierte
Kommunikationsprimitive des OS wie ich oben schon geschrieben habe: es
gibt genug davon.
Wilhelm M. schrieb:> Würde ich nicht machen, damit wirst Du nicht glücklich.> Dann kannst Du auch gleich statt der Coroutine eine FSM in einem eigenen> Thread laufen lassen, die eine Coroutine simuliert. Das ist state-of-the-art.
Warum sollte man das tun wollen? Das ist komplizierter, und hat
vergleichsweise sehr hohe synchronisierungs und context switching
kosten.
> Natürlich kann C keine richtigen Coroutinen und man> hat nicht die Schlüsselworte co_await, co_yield und co_return.
Die lassen sich als simple wrapper Funktionen um die swapcontext
Funktionen implementieren. Und im Gegensatz zu C++ Coroutinen, könnte
man die überall haben, auch in callbacks usw. Da gibt es dann auch
genauso keine manuellen Verrenkungen mit FSMs, wie es sie auch in C++
nicht braucht.
Wilhelm M. schrieb:>> Wilhelm M. schrieb:>>> In C++ (20) ist das natürlich "etwas" eleganter>>>> Naja, da hat man dafür wieder die Limitation mit der Unterscheidung>> zwischen normalen Funktionen und diesen Funktionen.>> Wo soll da eine "Begrenzung" sein: man gewinnt ja gerade das> coroutine-Konzept hinzu!
Dafür braucht es nicht viel, das ist in C kein Problem. Das swapcontext
Zeug ist der komplexe teil, den man selbst nicht in reinem C
implementieren könnte - aber eben, da gibt es gute Libraries, die per
ASM oder per OS syscalls das übernehmen.
Sachen wie co_await, co_yield und co_return in c++ nicht in normalen
Unterfunktionen und Callbacks verwenden zu können, ist eine massive
Einschränkung, denn das schränkt ein, wo man an seine Daten kommt /
warten kann, und sie generieren / weitergeben kann, ohne sich wieder um
den Zustand oder um Queues kümmern zu müssen.
Das heisst aber nicht, dass ich Stack basierte Coroutinen in C besser
finde als in C++. Der massiv kleinere und konstante & bekannte
Speicherverbrauch⚹ bei den C++ coroutinen werden diese viel besser
skalieren lassen, tausende davon dürften quasi gratis sein. Dieser
Vorteil wird die Nachteile meistens bei weitem überwiegen. Einen
vergleichbaren Speicherverbrauch wird man in C nur bekommen, wenn man
das mit einer FSM und ohne ganzen Stack nachbildet, was dann natürlich
wieder weniger elegant wäre.
Trotzdem sind die C++ Coroutinen aber weniger stark und Flexibel als
stack basierte C coroutinen. Alles kann man nicht haben.
⚹ In C, mit pth_uctx_make, und vermutlich auch dem POSIX equivalent, ist
die minimale stack grösse 16384 (16KB). Nach 64 Coroutinen ist schon 1
MB weg. Bei nur 1024 Subroutinen sind 16MB weg. Der C++ Coroutinen
Status hingegen dürfte oft weit unter 1KB liegen, vielleicht ein paar
hundert Bytes wenn man ordentlich Zustand da rein packt.
Solange dein Problem so einfach ist, dass du einen statischen
Datenfluss-Graphen hast, passt der Ansatz.
🐧 DPA 🐧 schrieb:> Gibt es da für> C irgend ein allgemeines Pattern, oder eine Library oder so, mit dem man> alle Fälle erschlagen kann?
Aber wenns "allgemein" sein soll, dann soll es bestimmt auch möglich
sein, den Datenfluss-Graphen zur Laufzeit des Programms zu ändern. Pipes
woanders hin biegen, Knoten löschen, Knoten neu erzeugen, usw. Und zwar
auch durch einen der Datenverarbeitungs-Knoten selbst. Und da wird es
haarig. Nicht nur in der Implementierung, sondern schon allein in der
Spezifikation was in diesem Fall passieren soll.