Forum: PC-Programmierung Programmdesign: Datenverarbeitung: Streams: Push vs. Pull


von 🐧 DPA 🐧 (Gast)


Lesenswert?

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
  process data
4
  return processed data
5
}
2) pushing
1
processData(data){
2
  process data
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...

von Jan K. (jan_k776)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

🐧 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).

von 🐧 DPA 🐧 (Gast)


Lesenswert?

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.

von 🐧 DPA 🐧 (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

🐧 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).

von c-hater (Gast)


Lesenswert?

🐧 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...

von Rolf M. (rmagnus)


Lesenswert?

🐧 DPA 🐧 schrieb:
> Das Problem ist aber, unter anderem, die zusammenzuhängen.

https://en.wikipedia.org/wiki/Coroutine#Subroutines

von Wilhelm M. (wimalopaan)


Lesenswert?

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).

: Bearbeitet durch User
von udok (Gast)


Lesenswert?

🐧 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....

von 🐧 DPA 🐧 (Gast)


Lesenswert?

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.

von Wilhelm M. (wimalopaan)


Lesenswert?

🐧 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.

von Daniel A. (daniel-a)


Lesenswert?

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.

: Bearbeitet durch User
von SebastianS (Gast)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.