it-swarm.com.de

Wann wäre eine Abfrage nach Ereignissen besser als die Verwendung eines Beobachtermusters?

Gibt es Szenarien, in denen die Abfrage nach Ereignissen besser wäre als die Verwendung des Beobachtermusters ? Ich habe Angst vor Umfragen und würde sie nur verwenden, wenn mir jemand ein gutes Szenario gegeben hätte. Ich kann mir nur vorstellen, wie das Beobachtermuster besser ist als das Abrufen. Betrachten Sie dieses Szenario:

Sie programmieren einen Autosimulator. Das Auto ist ein Objekt. Sobald sich das Auto einschaltet, möchten Sie einen "vroom vroom" -Soundclip abspielen.

Sie können dies auf zwei Arten modellieren:

Polling : Das Autoobjekt wird jede Sekunde abgefragt, um festzustellen, ob es eingeschaltet ist. Wenn es eingeschaltet ist, spielen Sie den Soundclip ab.

Beobachtermuster : Machen Sie das Auto zum Subjekt des Beobachtermusters. Lassen Sie das Ereignis "Ein" für alle Beobachter veröffentlichen, wenn es sich selbst einschaltet. Erstellen Sie ein neues Soundobjekt, das auf das Auto hört. Lassen Sie den Rückruf "Ein" implementieren, mit dem der Soundclip abgespielt wird.

In diesem Fall denke ich, dass das Beobachtermuster gewinnt. Erstens ist das Abrufen prozessorintensiver. Zweitens wird der Soundclip beim Einschalten des Autos nicht sofort ausgelöst. Aufgrund des Abfragezeitraums kann es zu einer Lücke von bis zu 1 Sekunde kommen.

41
JoJo

Stellen Sie sich vor, Sie möchten über jeden Motorzyklus informiert werden, z. um dem Fahrer eine Drehzahlmessung anzuzeigen.

Beobachtermuster : Die Engine veröffentlicht für jeden Zyklus ein "Motorzyklus" -Ereignis für alle Beobachter. Erstellen Sie einen Listener, der Ereignisse zählt und die RPM-Anzeige aktualisiert.

Polling : Die Drehzahlanzeige fragt den Motor in regelmäßigen Abständen nach einem Motorzykluszähler und aktualisiert die Drehzahlanzeige entsprechend.

In diesem Fall würde sich das Beobachtermuster wahrscheinlich verlieren: Der Motorzyklus ist ein hochfrequenter Prozess mit hoher Priorität. Sie möchten diesen Prozess nicht verzögern oder blockieren, nur um eine Anzeige zu aktualisieren. Sie möchten den Thread-Pool auch nicht mit Motorzyklusereignissen überladen.


PS : Ich verwende das Abfragemuster auch häufig in der verteilten Programmierung:

Beobachtermuster : Prozess A sendet eine Nachricht an Prozess B, die besagt, dass "jedes Mal, wenn ein Ereignis E eintritt, eine Nachricht an Prozess A gesendet wird".

Abfragemuster : Prozess A sendet regelmäßig eine Nachricht an Prozess B mit der Meldung "Wenn Ihr Ereignis E seit der letzten Abfrage aufgetreten ist, senden Sie mir jetzt eine Nachricht".

Das Abfragemuster erzeugt etwas mehr Netzwerklast. Das Beobachtermuster hat aber auch Nachteile:

  • Wenn Prozess A abstürzt, wird er sich niemals abmelden, und Prozess B wird versuchen, Benachrichtigungen für alle Ewigkeit an ihn zu senden, es sei denn, er kann Remote-Prozessfehler zuverlässig erkennen (was nicht einfach ist).
  • Wenn Ereignis E sehr häufig ist und/oder die Benachrichtigungen viele Daten enthalten, erhält Prozess A möglicherweise mehr Ereignisbenachrichtigungen, als er verarbeiten kann. Mit dem Abfragemuster kann es nur die Abfrage drosseln.
  • Im Beobachtermuster kann eine hohe Last "Wellen" im gesamten System verursachen. Wenn Sie Sperrbuchsen verwenden, können diese Wellen in beide Richtungen gehen.
55
nikie

Das Abrufen ist besser, wenn der Abrufprozess erheblich langsamer abläuft als die Abfragen. Wenn Sie Ereignisse in eine Datenbank schreiben, ist es häufig besser, alle Ihre Ereignisproduzenten abzufragen, alle Ereignisse zu erfassen, die seit der letzten Abfrage aufgetreten sind, und sie dann in einer einzigen Transaktion zu schreiben. Wenn Sie versuchen, jedes Ereignis so zu schreiben, wie es aufgetreten ist, können Sie möglicherweise nicht mithalten und haben möglicherweise Probleme, wenn Ihre Eingabewarteschlangen voll sind. Dies ist auch in lose gekoppelten verteilten Systemen sinnvoller, in denen die Latenz hoch ist oder der Aufbau und Abbau von Verbindungen teuer ist. Ich finde Polling-Systeme einfacher zu schreiben und zu verstehen, aber in den meisten Situationen scheinen Beobachter oder ereignisgesteuerte Verbraucher (meiner Erfahrung nach) eine bessere Leistung zu bieten.

7
TMN

Das Abrufen ist viel einfacher, um über ein Netzwerk zu arbeiten, wenn Verbindungen fehlschlagen, Server möglicherweise nicht mehr funktionieren usw. Denken Sie am Ende des Tages daran, dass ein TCP Socket erforderlich ist) Keep-a-Live-Nachrichten werden abgefragt, andernfalls geht der Server davon aus, dass der Client verschwunden ist.

Polling ist auch gut, wenn Sie eine Benutzeroberfläche auf dem neuesten Stand halten möchten, aber die zugrunde liegenden Objekte sehr schnell ändern, es macht keinen Sinn, die Benutzeroberfläche in den meisten Apps mehr als ein paar Mal pro Sekunde zu aktualisieren.

Vorausgesetzt, der Server kann zu sehr geringen Kosten auf "keine Änderung" antworten und Sie nicht zu oft abfragen und Sie haben nicht Tausende von Clients, die abfragen, funktioniert das Abrufen im wirklichen Leben sehr gut.

In Fällen von "im Speicher" verwende ich standardmäßig das Beobachtermuster, da dies normalerweise die geringste Arbeit ist.

7
Ian

Umfragen haben einige Nachteile, die Sie im Grunde bereits in Ihrer Frage angegeben haben.

Es kann jedoch eine bessere Lösung sein, wenn Sie das Beobachtbare wirklich von jedem Beobachter entkoppeln möchten. Manchmal ist es jedoch besser, einen beobachtbaren Wrapper für das zu beobachtende Objekt in solchen Fällen zu verwenden.

Ich würde Polling nur verwenden, wenn das Observable bei Objektinteraktionen nicht beobachtet werden kann. Dies ist häufig der Fall, wenn beispielsweise Datenbanken abgefragt werden, bei denen keine Rückrufe möglich sind. Ein weiteres Problem könnte Multithreading sein, bei dem es oftmals sicherer ist, Nachrichten abzufragen und zu verarbeiten, als Objekte direkt aufzurufen, um Parallelitätsprobleme zu vermeiden.

5
Falcon

Ein gutes Beispiel dafür, wann die Abfrage die Benachrichtigung übernimmt, finden Sie in den Netzwerkstapeln des Betriebssystems.

Für Linux war es eine große Sache, als der Netzwerkstapel NAPI aktivierte, eine Netzwerk-API, mit der die Treiber von einem Interrupt-Modus (Benachrichtigung) in einen Polling-Modus wechseln konnten.

Bei mehreren Gigabit-Ethernet-Schnittstellen überlasten die Interrupts häufig die CPU, wodurch das System langsamer lief, als es sollte. Beim Abrufen sammeln die Netzwerkkarten Pakete in Puffern, bis sie abgefragt werden, oder die Karten schreiben die Pakete sogar über DMA in den Speicher. Wenn das Betriebssystem bereit ist, fragt es die Karte nach allen Daten ab und führt die Standard-TCP/IP-Verarbeitung durch.

Der Abfragemodus ermöglicht es der CPU, Ethernet-Daten mit ihrer maximalen Verarbeitungsrate ohne unnötige Interrupt-Last zu erfassen. Der Interrupt-Modus ermöglicht es der CPU, zwischen Paketen im Leerlauf zu arbeiten, wenn die Arbeit nicht so beschäftigt ist.

Das Geheimnis ist, wann man von einem Modus in den anderen wechselt. Jeder Modus hat Vorteile und sollte an der richtigen Stelle verwendet werden.

4
Zan Lynx

Ich liebe Umfragen! Mache ich Ja! Mache ich Ja! Mache ich Ja! Mache ich noch Ja! Was ist mit jetzt? Ja!

Wie andere bereits erwähnt haben, kann es unglaublich ineffizient sein, wenn Sie nur abfragen, um immer wieder denselben unveränderten Status zu erhalten. Dies ist ein Rezept, um CPU-Zyklen zu verbrennen und die Akkulaufzeit auf Mobilgeräten erheblich zu verkürzen. Natürlich ist es nicht verschwenderisch, wenn Sie jedes Mal einen neuen und aussagekräftigen Zustand mit einer Geschwindigkeit wiedererlangen, die nicht schneller als gewünscht ist.

Aber der Hauptgrund, warum ich Umfragen liebe, ist die Einfachheit und Vorhersehbarkeit. Sie können den Code nachverfolgen und leicht sehen, wann und wo und in welchem ​​Thread etwas passieren wird. Wenn wir theoretisch in einer Welt leben würden, in der Umfragen eine vernachlässigbare Verschwendung sind (obwohl die Realität weit davon entfernt ist), dann würde dies meiner Meinung nach die Pflege von Code vereinfachen. Und das ist der Vorteil des Abrufens und Ziehens, da ich sehe, ob wir die Leistung außer Acht lassen können, obwohl wir dies in diesem Fall nicht tun sollten.

Als ich in der DOS-Ära mit dem Programmieren anfing, drehten sich meine kleinen Spiele um Umfragen. Ich habe einen Assembly-Code aus einem Buch kopiert, das ich in Bezug auf Tastatur-Interrupts kaum verstanden habe, und einen Puffer mit Tastaturzuständen gespeichert. Zu diesem Zeitpunkt hat meine Hauptschleife immer abgefragt. Ist die Aufwärts-Taste gedrückt? Nee. Ist die Aufwärts-Taste gedrückt? Nee. Wie wäre es jetzt? Nee. Jetzt? Ja. Okay, beweg den Spieler.

Und obwohl es unglaublich verschwenderisch war, fand ich es viel einfacher, darüber nachzudenken, als dies heutzutage bei Multitasking und ereignisgesteuerter Programmierung der Fall ist. Ich wusste zu jeder Zeit genau, wann und wo etwas passieren würde, und es war einfacher, die Frameraten ohne Schluckauf stabil und vorhersehbar zu halten.

Seitdem habe ich immer versucht, einen Weg zu finden, um einige der Vorteile und Vorhersagbarkeit davon zu nutzen, ohne die CPU-Zyklen tatsächlich zu verbrennen, wie z. B. die Verwendung von Bedingungsvariablen, um Threads zu benachrichtigen, um aufzuwachen, an welchem ​​Punkt sie den neuen Status abrufen können. Mach ihr Ding und schlafe wieder ein und warte darauf, wieder benachrichtigt zu werden.

Und irgendwie finde ich es viel einfacher, mit Ereigniswarteschlangen zu arbeiten, als mit Beobachtermustern, obwohl sie es immer noch nicht so einfach machen, vorherzusagen, wohin Sie gehen oder was am Ende passieren wird. Sie zentralisieren zumindest den Ablauf der Ereignisbehandlungssteuerung auf einige Schlüsselbereiche im System und behandeln diese Ereignisse immer im selben Thread, anstatt plötzlich von einer Funktion an einen völlig entfernten und unerwarteten Ort außerhalb eines zentralen Ereignisbehandlungs-Threads zu springen. Die Dichotomie muss also nicht immer zwischen Beobachtern und Umfragen bestehen. Event-Warteschlangen sind dort eine Art Mittelweg.

Aber ja, irgendwie fällt es mir so viel leichter, über Systeme nachzudenken, die Dinge tun, die analog zu den vorhersehbaren Kontrollflüssen sind, die ich vor langer Zeit bei Umfragen hatte, und gleichzeitig der Tendenz entgegenzuwirken, dass die Arbeit bei stattfindet Zeiten, in denen keine Statusänderungen aufgetreten sind. Es gibt also diesen Vorteil, wenn Sie dies auf eine Weise tun können, die CPU-Zyklen nicht unnötig brennt, wie dies bei Bedingungsvariablen der Fall ist.

Homogene Schleifen

In Ordnung, ich habe einen großartigen Kommentar von Josh Caswell Erhalten, der in meiner Antwort auf eine gewisse Dummheit hinwies:

"Wie die Verwendung von Bedingungsvariablen, um Threads zum Aufwecken zu benachrichtigen" Klingt nach einer ereignisbasierten/Beobachter-Anordnung, nicht nach einer Abfrage

Technisch gesehen wendet die Bedingungsvariable selbst das Beobachtermuster an, um Threads aufzuwecken/zu benachrichtigen. Das "Polling" zu nennen, wäre wahrscheinlich unglaublich irreführend. Aber ich finde, dass es einen ähnlichen Vorteil bietet, den ich als Umfrage aus den DOS-Tagen gefunden habe (nur in Bezug auf Kontrollfluss und Vorhersagbarkeit). Ich werde versuchen, es besser zu erklären.

Was ich damals ansprechend fand, war, dass man sich einen Codeabschnitt ansehen oder ihn nachverfolgen und sagen konnte: "Okay, dieser gesamte Abschnitt ist der Behandlung von Tastaturereignissen gewidmet. Sonst nichts wird in diesem Codeabschnitt passieren. Und ich weiß genau, was vorher passieren wird, und ich weiß genau, was danach passieren wird (Physik und Rendering, zB). " Die Abfrage von Die Tastaturzustände haben Ihnen diese Art der Zentralisierung des Kontrollflusses ermöglicht, was die Reaktion auf dieses externe Ereignis betrifft. Wir haben nicht sofort auf dieses externe Ereignis reagiert. Wir haben nach Belieben darauf reagiert.

Wenn wir ein Push-basiertes System verwenden, das auf einem Observer-Muster basiert, verlieren wir häufig diese Vorteile. Die Größe eines Steuerelements wird möglicherweise geändert, wodurch ein Größenänderungsereignis ausgelöst wird. Wenn wir es nachverfolgen, stellen wir fest, dass wir uns in exotischer Kontrolle befinden, die viele benutzerdefinierte Dinge in ihrer Größenänderung ausführt, wodurch mehr Ereignisse ausgelöst werden. Wir sind völlig überrascht, wie wir all diese kaskadierenden Ereignisse verfolgen, wo wir im System landen. Darüber hinaus stellen wir möglicherweise fest, dass all dies in einem bestimmten Thread nicht einmal konsistent auftritt, da Thread A hier möglicherweise die Größe eines Steuerelements ändert, während Thread B später auch die Größe eines Steuerelements ändert. Daher fand ich es immer sehr schwierig, darüber nachzudenken, wie schwierig es ist, vorherzusagen, wo alles passiert und was passieren wird.

Die Ereigniswarteschlange ist für mich etwas einfacher zu begründen, da sie zumindest auf Thread-Ebene vereinfacht, wo all diese Dinge passieren. Es könnten jedoch viele unterschiedliche Dinge passieren. Eine Ereigniswarteschlange könnte eine eklektische Mischung von zu verarbeitenden Ereignissen enthalten, und jedes Ereignis könnte uns immer noch überraschen, welche Kaskade von Ereignissen aufgetreten ist, in welcher Reihenfolge sie verarbeitet wurden und wie wir am Ende überall in der Codebasis herumspringen .

Was ich als "am nächsten" zum Abrufen betrachte, würde keine Ereigniswarteschlange verwenden, sondern eine sehr homogene Art der Verarbeitung verschieben. Ein PaintSystem wird möglicherweise durch eine Bedingungsvariable benachrichtigt, dass Malerarbeiten zum Neulackieren bestimmter Gitterzellen eines Fensters ausgeführt werden müssen. An diesem Punkt führt es eine einfache sequentielle Schleife durch die Zellen durch und malt alles darin in der richtigen z- neu. Auftrag. Möglicherweise gibt es hier eine Ebene für den Aufruf der Indirektion/des dynamischen Versands, um die Paint-Ereignisse in jedem Widget auszulösen, das sich in einer Zelle befindet, die neu gestrichen werden muss, aber das war's - nur eine Ebene indirekter Aufrufe. Die Bedingungsvariable verwendet das Beobachtermuster, um das PaintSystem darauf aufmerksam zu machen, dass es Arbeit zu erledigen hat, gibt jedoch nichts weiter an, und das PaintSystem ist einer einheitlichen, sehr homogenen Methode gewidmet Aufgabe an diesem Punkt. Wenn wir den Code PaintSystem's Debuggen und nachverfolgen, wissen wir, dass außer dem Malen nichts anderes passieren wird.

Es geht also hauptsächlich darum, das System dahin zu bringen, wo diese Dinge homogene Schleifen über Daten ausführen, wobei eine sehr singuläre Verantwortung darauf angewendet wird, anstatt inhomogene Schleifen über unterschiedliche Datentypen, die zahlreiche Verantwortlichkeiten ausführen, wie dies bei der Verarbeitung von Ereigniswarteschlangen der Fall sein kann.

Wir streben Folgendes an:

when there's work to do:
   for each thing:
       apply a very specific and uniform operation to the thing

Im Gegensatz zu:

when one specific event happens:
    do something with relevant thing
in relevant thing's event:
    do some more things
in thing1's triggered by thing's event:
    do some more things
in thing2's event triggerd by thing's event:
    do some more things:
in thing3's event triggered by thing2's event:
    do some more things
in thing4's event triggered by thing1's event:
    cause a side effect which shouldn't be happening
    in this order or from this thread.

Und so weiter. Und es muss nicht ein Thread pro Aufgabe sein. Ein Thread wendet möglicherweise Layouts (Größenänderung/Neupositionierung) für GUI-Steuerelemente an und malt sie neu, verarbeitet jedoch möglicherweise keine Tastatur- oder Mausklicks. Sie können dies also nur als Verbesserung der Homogenität einer Ereigniswarteschlange betrachten. Wir müssen jedoch auch keine Ereigniswarteschlange verwenden und die Größenänderungs- und Malfunktionen verschachteln. Wir können mögen:

in thread dedicated to layout and painting:
    when there's work to do:
         for each widget that needs resizing/reposition:
              resize/reposition thing to target size/position
              mark appropriate grid cells as needing repainting
         for each grid cell that needs repainting:
              repaint cell
         go back to sleep

Der obige Ansatz verwendet also nur eine Bedingungsvariable, um den Thread zu benachrichtigen, wenn Arbeit zu erledigen ist, verschachtelt jedoch nicht verschiedene Arten von Ereignissen (Größenänderung in einer Schleife, Malen in einer anderen Schleife, keine Mischung aus beiden) und nicht. Machen Sie sich nicht die Mühe zu kommunizieren, was genau zu tun ist (der Thread "entdeckt" dies beim Aufwachen, indem er sich die systemweiten Zustände des ECS ansieht). Jede Schleife ist dann sehr homogen, was es einfach macht, über die Reihenfolge nachzudenken, in der alles passiert.

Ich bin mir nicht sicher, wie ich diesen Ansatz nennen soll. Ich habe noch keine anderen GUI-Engines gesehen, die dies tun, und es ist eine Art meiner eigenen exotischen Herangehensweise an meine. Aber bevor ich versuchte, Multithread-GUI-Frameworks mithilfe von Beobachtern oder Ereigniswarteschlangen zu implementieren, hatte ich enorme Schwierigkeiten beim Debuggen und stieß auf einige obskure Rennbedingungen und Deadlocks, die ich nicht so intelligent beheben konnte, dass ich mich sicher fühlte über die Lösung (einige Leute könnten dies tun, aber ich bin nicht klug genug). Bei meinem ersten Iterationsdesign wurde nur ein Slot direkt durch ein Signal gerufen, und einige Slots erzeugten dann andere Threads, um asynchrone Arbeit zu leisten. Das war am schwierigsten zu begründen, und ich stolperte über Rennbedingungen und Deadlocks. Bei der zweiten Iteration wurde eine Ereigniswarteschlange verwendet, und das war etwas einfacher zu überlegen, aber für mein Gehirn nicht einfach genug, um dies zu tun, ohne immer noch in den dunklen Stillstand und die Rassenbedingungen zu geraten. Die dritte und letzte Iteration verwendete den oben beschriebenen Ansatz, und schließlich konnte ich ein Multithread-GUI-Framework erstellen, das selbst ein dummer Simpleton wie ich korrekt implementieren konnte.

Diese Art des endgültigen Multithread-GUI-Designs ermöglichte es mir, etwas anderes zu finden, das so viel einfacher zu überlegen war, und diese Art von Fehlern zu vermeiden, die ich tendenziell machte, und einer der Gründe, warum ich es so viel einfacher fand, darüber nachzudenken Das liegt am wenigsten an diesen homogenen Schleifen und daran, wie sie dem Kontrollfluss ähnelten, ähnlich wie bei den Abfragen in den DOS-Tagen (obwohl es nicht wirklich abgefragt wird und nur dann Arbeit ausführt, wenn noch Arbeit zu erledigen ist). Die Idee war, sich so weit wie möglich vom Ereignisbehandlungsmodell zu entfernen, das inhomogene Schleifen, inhomogene Nebenwirkungen und inhomogene Kontrollflüsse impliziert, und immer mehr auf homogene Schleifen hinzuarbeiten, die einheitlich mit homogenen Daten arbeiten und diese isolieren und Vereinheitlichung von Nebenwirkungen auf eine Weise, die es einfacher machte, sich nur auf "was" zu konzentrieren, nicht auf "wann" und "wo", um über die Richtigkeit des Codes nachzudenken.

2
user204677