Unterschied zwischen Konsument / Produzent und Beobachter / Beobachtbar

15

Ich arbeite am Design einer Anwendung, die aus drei Teilen besteht:

  • Ein einzelner Thread, der bestimmte Ereignisse überwacht (Dateierstellung, externe Anforderungen usw.)
  • N Worker-Threads, die auf diese Ereignisse reagieren, indem sie sie verarbeiten (jeder Worker verarbeitet und verbraucht ein einzelnes Ereignis, und die Verarbeitung kann eine variable Zeit dauern)
  • einen Controller, der diese Threads verwaltet und die Fehlerbehandlung durchführt (Neustart der Threads, Protokollierung der Ergebnisse)

Obwohl dies ziemlich einfach und nicht schwer zu implementieren ist, frage ich mich, was der "richtige" Weg wäre, dies zu tun (in diesem konkreten Fall in Java, aber auch Antworten auf höhere Abstraktionen werden geschätzt). Zwei Strategien kommen in den Sinn:

  • Observer / Observable: Der beobachtende Thread wird vom Controller beobachtet. Im Falle eines Ereignisses wird der Controller benachrichtigt und kann die neue Aufgabe einem freien Thread aus einem wiederverwendbaren zwischengespeicherten Thread-Pool zuweisen (oder die Aufgaben in der FIFO-Warteschlange abwarten und zwischenspeichern, wenn alle Threads gerade beschäftigt sind). Die Worker-Threads implementieren Callable und geben entweder erfolgreich das Ergebnis (oder einen booleschen Wert) zurück oder geben mit einem Fehler zurück. In diesem Fall kann der Controller entscheiden, was zu tun ist (abhängig von der Art des aufgetretenen Fehlers).

  • Produzent / Konsument : Der überwachende Thread teilt eine BlockingQueue mit dem Controller (Ereigniswarteschlange) und der Controller teilt zwei mit allen Workern (Taskwarteschlange und Ergebniswarteschlange). Im Falle eines Ereignisses stellt der überwachende Thread ein Task-Objekt in die Ereigniswarteschlange. Der Controller entnimmt neue Aufgaben aus der Ereigniswarteschlange, überprüft sie und stellt sie in die Aufgabenwarteschlange. Jeder Mitarbeiter wartet auf neue Aufgaben und entnimmt / verbraucht diese aus der Aufgabenwarteschlange (wer zuerst kommt mahlt zuerst, wird von der Warteschlange selbst verwaltet) und stellt die Ergebnisse oder Fehler zurück in die Ergebniswarteschlange. Schließlich kann die Steuerung die Ergebnisse aus der Ergebniswarteschlange abrufen und im Fehlerfall die entsprechenden Schritte ausführen.

Die Endergebnisse beider Ansätze sind ähnlich, unterscheiden sich jedoch geringfügig:

Mit Observers erfolgt die Steuerung von Threads direkt und jede Aufgabe wird einem bestimmten neuen Worker zugeordnet. Der Aufwand für die Erstellung von Threads ist möglicherweise höher, aber dank des zwischengespeicherten Thread-Pools nicht sehr hoch. Auf der anderen Seite wird das Observer-Muster auf einen einzelnen Observer anstatt auf mehrere reduziert, wofür es nicht genau entwickelt wurde.

Die Warteschlangenstrategie scheint einfacher zu erweitern zu sein, zum Beispiel ist das Hinzufügen mehrerer Produzenten anstelle von einem einfach und erfordert keine Änderung. Der Nachteil ist, dass alle Threads unbegrenzt ausgeführt werden, selbst wenn überhaupt keine Arbeit ausgeführt wird, und die Fehler- / Ergebnisbehandlung nicht so elegant aussieht wie in der ersten Lösung.

Welcher Ansatz passt in dieser Situation am besten und warum? Ich fand es schwierig, online Antworten auf diese Frage zu finden, da die meisten Beispiele nur eindeutige Fälle behandeln, z. B. das Aktualisieren vieler Fenster mit einem neuen Wert im Observer-Fall oder das Verarbeiten mit mehreren Verbrauchern und Produzenten. Jede Eingabe wird sehr geschätzt.

user183536
quelle

Antworten:

10

Sie stehen kurz davor, Ihre eigene Frage zu beantworten. :)

Im Observable / Observer-Muster (Flip beachten) sind drei Dinge zu beachten:

  1. Im Allgemeinen ist die Benachrichtigung über die Änderung, dh "Nutzlast", in der beobachtbaren.
  2. Das Beobachtbare existiert .
  3. Die Beobachter müssen dem vorhandenen Observablen bekannt sein (sonst haben sie nichts zu beobachten).

Indem diese Punkte kombiniert werden, bedeutet dies, dass das Observable seine nachgeordneten Komponenten kennt, dh die Beobachter. Der Datenfluss wird von Natur aus vom Beobachtbaren gesteuert - Beobachter "leben und sterben" nur durch das, worauf sie beobachten.

Im Producer / Consumer-Muster erhalten Sie eine ganz andere Interaktion:

  1. Grundsätzlich besteht die Nutzlast unabhängig vom Hersteller, der sie produziert.
  2. Die Produzenten wissen nicht, wie oder wann die Verbraucher aktiv sind.
  3. Verbraucher müssen den Hersteller der Nutzlast nicht kennen.

Der Datenfluss ist jetzt vollständig zwischen einem Produzenten und einem Konsumenten getrennt - der Produzent weiß nur, dass er einen Output hat, und der Konsument weiß nur, dass er einen Input hat. Wichtig ist, dass Produzenten und Konsumenten ohne die Anwesenheit des anderen existieren können.

Ein weiterer nicht allzu subtiler Unterschied besteht darin, dass mehrere Beobachter auf demselben Observable normalerweise dieselbe Nutzlast erhalten (es sei denn, es liegt eine unkonventionelle Implementierung vor), während dies bei mehreren Verbrauchern desselben Produzenten möglicherweise nicht der Fall ist. Dies hängt davon ab, ob es sich bei dem Vermittler um einen warteschlangen- oder themenbezogenen Ansatz handelt. Ersteres übergibt für jeden Konsumenten eine andere Nachricht, während Letzteres sicherstellt (oder versucht), dass alle Konsumenten auf Nachrichtenbasis verarbeiten.

So fügen Sie sie in Ihre Anwendung ein:

  • Im Observable / Observer-Muster muss Ihr überwachender Thread wissen, wie er den Controller informiert, wenn er initialisiert wird. Als Beobachter wartet der Controller wahrscheinlich auf eine Benachrichtigung vom überwachenden Thread, bevor die Threads die Änderung verarbeiten können.
  • Im Producer / Consumer-Muster muss Ihr Beobachtungsthread nur das Vorhandensein der Ereigniswarteschlange kennen und interagiert ausschließlich damit. Als Consumer fragt der Controller dann die Ereigniswarteschlange ab und sobald er eine neue Nutzlast erhält, lässt er die Threads damit umgehen.

Um Ihre Frage direkt zu beantworten: Wenn Sie ein gewisses Maß an Trennung zwischen Ihrem Überwachungs-Thread und Ihrem Controller aufrechterhalten möchten, damit Sie diese unabhängig voneinander bedienen können, sollten Sie sich dem Muster Producer / Consumer zuwenden.

hjk
quelle
2
Vielen Dank für Ihre ausführliche Antwort. Leider kann ich es wegen fehlender Reputation nicht verbessern und habe es stattdessen als Lösung markiert. Die von Ihnen erwähnte zeitliche Unabhängigkeit zwischen beiden Teilen ist etwas Positives, an das ich bis jetzt nicht gedacht habe. Warteschlangen können kurze Bursts von vielen Ereignissen mit langen Pausen zwischen den Ereignissen viel besser verwalten als die direkte Aktion nach der Beobachtung von Ereignissen (wenn die maximale Thread-Anzahl festgelegt und relativ niedrig ist). Die Anzahl der Threads kann abhängig von der Anzahl der aktuellen Warteschlangenelemente auch dynamisch erhöht / verringert werden.
user183536
@ user183536 Keine Probleme, gerne helfen! :)
hjk