Gibt es Szenarien, in denen die Abfrage nach Ereignissen besser wäre als die Verwendung des Beobachtermusters ? Ich habe Angst vor der Verwendung von Polling und würde es nur verwenden, wenn mir jemand ein gutes Szenario gibt. Ich kann mir nur vorstellen, wie das Beobachtermuster besser ist als die Befragung. Betrachten Sie dieses Szenario:
Sie programmieren einen Autosimulator. Das Auto ist ein Objekt. Sobald sich das Auto einschaltet, möchten Sie einen Soundclip "vroom vroom" abspielen.
Sie können dies auf zwei Arten modellieren:
Polling : Pollen Sie das Autoobjekt jede Sekunde ab, um festzustellen, ob es eingeschaltet ist. Wenn es eingeschaltet ist, spielen Sie den Soundclip ab.
Beobachtermuster : Machen Sie das Auto zum Motiv des Beobachtermusters. Lassen Sie es das "Ein" -Ereignis für alle Beobachter veröffentlichen, wenn es sich selbst einschaltet. Erstellen Sie ein neues Soundobjekt, das auf das Auto hört. Lass es den "on" Callback implementieren, der den Soundclip abspielt.
In diesem Fall, denke ich, gewinnt das Beobachtermuster. Erstens ist die Abfrage prozessorintensiver. Zweitens wird der Soundclip beim Einschalten des Autos nicht sofort ausgelöst. Aufgrund des Abfragezeitraums kann eine Pause von bis zu 1 Sekunde auftreten.
quelle
Antworten:
Stellen Sie sich vor, Sie möchten über jeden Motorzyklus informiert werden, z. B. um dem Fahrer eine Drehzahlmessung anzuzeigen.
Beobachtermuster: Der Motor 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.
Abfrage: 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 lösen: Der Motorzyklus ist ein hochfrequenter Prozess mit hoher Priorität. Sie möchten diesen Prozess nicht verzögern oder anhalten, nur um eine Anzeige zu aktualisieren. Sie möchten den Thread-Pool auch nicht mit Motorzyklusereignissen überschwemmen.
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 Ereignis E seit dem letzten Abruf aufgetreten ist, senden Sie mir jetzt eine Nachricht".
Das Abfragemuster führt zu einer etwas höheren Netzwerklast. Aber das Beobachtermuster hat auch Nachteile:
quelle
Polling ist besser, wenn der Polling-Prozess erheblich langsamer abläuft als die Polling-Vorgänge. 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 eine einzige Transaktion zu schreiben. Wenn Sie versuchen, jedes Ereignis so zu schreiben, wie es aufgetreten ist, können Sie möglicherweise nicht mithalten und haben schließlich Probleme, wenn sich Ihre Eingabewarteschlangen füllen. Dies ist auch in lose gekoppelten verteilten Systemen sinnvoller, in denen die Latenzzeit hoch oder der Verbindungsaufbau und -abbau teuer sind. Meiner Meinung nach sind Abfragesysteme einfacher zu schreiben und zu verstehen, aber in den meisten Situationen scheinen Beobachter oder ereignisgesteuerte Verbraucher eine bessere Leistung zu bieten (meiner Erfahrung nach).
quelle
Polling ist viel einfacher, Arbeit zu bekommen über ein Netzwerk , wenn Verbindungen ausfallen können, Server gehen kann getan usw. Denken Sie daran , am Ende des Tages ein TCP - Socket „Polling“ Keep-a-Live - Nachrichten sonst der Server den Client benötigt werden nehmen ist weggegangen.
Polling ist auch dann sinnvoll, wenn Sie eine Benutzeroberfläche auf dem neuesten Stand halten möchten, sich die zugrunde liegenden Objekte jedoch sehr schnell ändern. In den meisten Apps ist es nicht sinnvoll, die Benutzeroberfläche mehr als ein paar Mal pro Sekunde zu aktualisieren.
Vorausgesetzt, der Server kann mit sehr geringen Kosten auf "keine Änderung" antworten, und Sie rufen nicht zu oft ab und es gibt nicht Tausende von Clients, die abrufen, dann funktioniert das Abrufen im wirklichen Leben sehr gut.
Für “ In-Memory ” -Fälle verwende ich jedoch standardmäßig das Beobachtermuster, da dies normalerweise die geringste Arbeit ist.
quelle
Umfragen haben einige Nachteile, die Sie im Grunde genommen bereits in Ihrer Frage angegeben haben.
Es kann jedoch eine bessere Lösung sein, wenn Sie das Observable wirklich von allen Beobachtern entkoppeln möchten. Manchmal ist es in solchen Fällen jedoch besser, einen beobachtbaren Wrapper für das zu beobachtende Objekt zu verwenden.
Ich würde Polling nur verwenden, wenn das Observable bei Objektinteraktionen nicht beobachtet werden kann, was häufig der Fall ist, wenn beispielsweise Datenbanken abgefragt werden, für die keine Rückrufe möglich sind. Ein weiteres Problem kann Multithreading sein, bei dem es oftmals sicherer ist, Nachrichten abzufragen und zu verarbeiten, als Objekte direkt aufzurufen, um Parallelitätsprobleme zu vermeiden.
quelle
Ein gutes Beispiel dafür, wie Abfragen von Benachrichtigungen übernommen werden, finden Sie in den Netzwerkstapeln des Betriebssystems.
Für Linux war es eine große Sache, als der Netzwerkstapel NAPI aktivierte, eine Netzwerk-API, die es den Treibern ermöglichte, von einem Interrupt-Modus (Benachrichtigung) in einen Polling-Modus zu wechseln.
Bei mehreren Gigabit-Ethernet-Schnittstellen überlasteten die Interrupts häufig die CPU und führten dazu, dass das System langsamer lief, als es sollte. Beim Abrufen sammeln die Netzwerkkarten Pakete in Puffern, bis sie abgerufen werden, oder die Karten schreiben die Pakete sogar über DMA in den Speicher. Wenn das Betriebssystem bereit ist, fragt es die Karte nach all ihren Daten ab und führt die Standard-TCP / IP-Verarbeitung durch.
Der Polling-Modus ermöglicht es der CPU, Ethernet-Daten mit maximaler Verarbeitungsrate ohne unnötige Interrupt-Belastung zu erfassen. Der Interrupt-Modus ermöglicht es der CPU, zwischen Paketen im Leerlauf zu sein, wenn die Arbeit nicht so beschäftigt ist.
Das Geheimnis ist, wann von einem Modus in den anderen gewechselt werden muss. Jeder Modus hat Vorteile und sollte an der richtigen Stelle verwendet werden.
quelle
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 keine Verschwendung, wenn Sie jedes Mal einen neuen und aussagekräftigen Zustand mit einer Geschwindigkeit zurückerhalten, die nicht schneller als gewünscht ist.
Aber der Hauptgrund, warum ich das Umfragen liebe, ist seine Einfachheit und Berechenbarkeit. Sie können den Code nachverfolgen und leicht sehen, wann und wo Dinge passieren und in welchem Thread. Wenn wir theoretisch in einer Welt leben würden, in der Umfragen eine vernachlässigbare Verschwendung darstellen (obwohl die Realität weit davon entfernt ist), würde dies meiner Meinung nach die Pflege des Codes erheblich vereinfachen. Und das ist der Vorteil von Polling und Pulling, 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 das Polling. Ich habe Assembler-Code aus einem Buch kopiert, das ich im Zusammenhang mit Tastaturinterrupts kaum verstanden habe, und einen Puffer mit Tastaturzuständen gespeichert, an dem meine Hauptschleife immer abrief. Ist die Auf-Taste gedrückt? Nee. Ist die Auf-Taste gedrückt? Nee. Wie wäre es jetzt? Nee. Jetzt? Ja. Okay, beweg den Spieler.
Und obwohl es unglaublich verschwenderisch war, fiel mir dies im Vergleich zu diesen Tagen mit Multitasking und ereignisgesteuerter Programmierung viel leichter. Ich wusste genau, wann und wo die Dinge zu jeder Zeit passieren würden, und es war einfacher, die Bildraten ohne Probleme stabil und vorhersehbar zu halten.
Seitdem habe ich immer versucht, einen Weg zu finden, um einige der Vorteile und Vorhersagbarkeiten davon zu erhalten, ohne die CPU-Zyklen tatsächlich zu verbrennen. Mach ihr Ding und schlafe wieder ein und warte darauf, wieder benachrichtigt zu werden.
Und irgendwie finde ich es viel einfacher, mit Ereignis-Warteschlangen zu arbeiten, als mit Beobachter-Mustern, 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 Befragten bestehen. Ereigniswarteschlangen sind dort eine Art Mittelweg.
Aber ja, irgendwie finde ich es viel einfacher, über Systeme nachzudenken, die Dinge tun, die den vorhersehbaren Kontrollabläufen, die ich vor langer Zeit beim Umfragen hatte, in ähnlicher Weise ähneln, und gleichzeitig der Tendenz entgegenzuwirken, dass die Arbeit dort stattfindet Zeiten, in denen keine Zustandsänderungen aufgetreten sind. Es ist also von Vorteil, wenn Sie dies auf eine Weise tun können, bei der CPU-Zyklen nicht unnötig verbrannt werden, wie dies bei Bedingungsvariablen der Fall ist.
Homogene Schleifen
Josh Caswell
Also gut , ich habe einen tollen Kommentar bekommen , der auf einige Dummheiten in meiner Antwort hinwies:Technisch gesehen wendet die Bedingungsvariable selbst das Beobachtermuster an, um Threads aufzuwecken / zu benachrichtigen, so dass die Bezeichnung "Polling" wahrscheinlich unglaublich irreführend wäre. Ich stelle jedoch fest, dass es einen ähnlichen Vorteil bietet wie das Abfragen aus DOS-Tagen (nur in Bezug auf Kontrollfluss und Vorhersagbarkeit). Ich werde versuchen, es besser zu erklären.
Was ich damals ansprechend fand, war, dass Sie sich einen Codeabschnitt ansehen oder ihn nachverfolgen und sagen konnten: "Okay, dieser gesamte Abschnitt ist der Behandlung von Tastaturereignissen gewidmet. In diesem Codeabschnitt wird nichts anderes passieren Und ich weiß genau, was vorher passieren wird, und ich weiß genau, was danach passieren wird (z. B. Physik und Rendering). " Durch die Abfrage der Tastaturzustände konnten Sie den Steuerungsfluss so zentralisieren, dass Sie nur noch handhaben konnten, was als Reaktion auf dieses externe Ereignis geschehen sollte. Wir haben nicht sofort auf dieses externe Ereignis reagiert. Wir haben darauf nach Belieben reagiert.
Wenn wir ein Push-basiertes System verwenden, das auf einem Observer-Muster basiert, verlieren wir oft diese Vorteile. Die Größe eines Steuerelements wird möglicherweise geändert, wodurch ein Größenänderungsereignis ausgelöst wird. Wenn wir es durchgehen, stellen wir fest, dass wir uns in einer exotischen Kontrolle befinden, die eine Menge benutzerdefinierter Dinge in ihrer Größenänderung ausführt, die mehr Ereignisse auslöst. Wir sind am Ende völlig überrascht, in all diesen kaskadierenden Ereignissen zu stecken, wo wir im System landen. Darüber hinaus stellen wir möglicherweise fest, dass all dies in keinem bestimmten Thread 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. Ich fand es immer sehr schwierig zu überlegen, wie schwierig es ist, vorherzusagen, wo alles passiert und was passieren wird.
Die Ereigniswarteschlange ist für mich etwas einfacher zu überlegen, weil sie vereinfacht, wo all diese Dinge zumindest auf Thread-Ebene passieren. Es könnten jedoch viele unterschiedliche Dinge passieren. Eine Ereigniswarteschlange könnte eine eklektische Mischung von Ereignissen enthalten, die verarbeitet werden müssen, und jedes einzelne könnte uns überraschen, welche Ereigniskaskade aufgetreten ist, in welcher Reihenfolge sie verarbeitet wurden und wie wir am Ende überall in der Codebasis abprallen .
Was ich für "am nächsten am Polling" halte, würde keine Ereigniswarteschlange verwenden, sondern eine sehr homogene Verarbeitung aufschieben. A wird
PaintSystem
möglicherweise durch eine Bedingungsvariable darauf aufmerksam gemacht, dass Malarbeiten erforderlich sind, um bestimmte Rasterzellen eines Fensters neu zu zeichnen. An diesem Punkt führt es eine einfache sequenzielle Schleife durch die Zellen durch und zeichnet alles in der richtigen z-Reihenfolge neu. Möglicherweise gibt es hier eine Ebene für den Aufruf von Indirection / Dynamic Dispatch, um die Paint-Ereignisse in jedem Widget auszulösen, das sich in einer Zelle befindet, die neu gezeichnet werden muss, aber das ist es - nur eine Ebene für indirekte Aufrufe. Die Bedingungsvariable verwendet das Beobachtermuster, um diePaintSystem
Aufgabe zu warnen, gibt jedoch nicht mehr als das anPaintSystem
widmet sich zu diesem Zeitpunkt einer einheitlichen, sehr homogenen Aufgabe. Wenn wir denPaintSystem's
Code debuggen und nachverfolgen , wissen wir, dass nichts anderes als Malen passieren wird.Es geht also hauptsächlich darum, das System dahin zu bringen, wo diese Dinge homogene Schleifen über Daten ausführen, und dabei eine sehr singuläre Verantwortung zu übernehmen, anstatt inhomogene Schleifen über unterschiedliche Datentypen, die zahlreiche Aufgaben ausführen, wie dies bei der Verarbeitung von Ereigniswarteschlangen der Fall sein kann.
Wir streben diese Art von Dingen an:
Im Gegensatz zu:
Und so weiter. Und es muss nicht ein Thread pro Aufgabe sein. Ein Thread wendet möglicherweise Layout-Logik (Größenänderung / Neupositionierung) für GUI-Steuerelemente an und zeichnet sie neu, kann jedoch Tastatur- oder Mausklicks nicht verarbeiten. Sie können dies als Verbesserung der Homogenität einer Ereigniswarteschlange betrachten. Wir müssen jedoch keine Ereigniswarteschlange verwenden und auch keine Funktionen zum Ändern der Größe und zum Zeichnen überlappen. Wir können machen wie:
Der obige Ansatz verwendet nur eine Bedingungsvariable, um den Thread zu benachrichtigen, wenn Arbeit zu erledigen ist, verschachtelt jedoch nicht verschiedene Arten von Ereignissen (Größe in einer Schleife ändern, in einer anderen Schleife malen, nicht eine Mischung aus beiden), und dies ist nicht der Fall. ' Ich muss nur mitteilen, was genau die Arbeit ist, die erledigt werden muss (der Thread "entdeckt" dies beim Aufwachen, indem er sich die systemweiten Zustände des ECS ansieht). Jede durchgeführte Schleife ist dann von Natur aus sehr homogen, was es einfach macht, über die Reihenfolge, in der alles geschieht, nachzudenken.
Ich bin mir nicht sicher, wie ich diesen Ansatz bezeichnen soll. Ich habe nicht gesehen, dass andere GUI-Engines dies tun, und es ist meine eigene exotische Herangehensweise an meine. Aber bevor ich versuchte, Multithread-GUI-Frameworks mithilfe von Beobachtern oder Ereigniswarteschlangen zu implementieren, hatte ich enorme Probleme beim Debuggen und stieß auch auf einige unklare Rennbedingungen und Deadlocks, die ich nicht klug genug war, um sie auf eine Weise zu beheben, die mich zuversichtlich machte über die Lösung (einige Leute könnten dies tun, aber ich bin nicht schlau genug). Mein erstes Iterationsdesign nannte einen Slot direkt durch ein Signal, und einige Slots brachten dann andere Threads hervor, um asynchrone Arbeit zu leisten. Das war am schwierigsten zu überlegen, und ich stolperte über Rennbedingungen und Deadlocks. Bei der zweiten Iteration wurde eine Ereigniswarteschlange verwendet. aber nicht leicht genug für mein Gehirn, es zu tun, ohne immer noch in die dunkle Sackgasse und in den Rennzustand zu geraten. Die dritte und letzte Iteration verwendete den oben beschriebenen Ansatz und ermöglichte es mir schließlich, ein Multithread-GUI-Framework zu erstellen, das selbst ein dummer Simpleton wie ich korrekt implementieren konnte.
Dann erlaubte mir diese Art des endgültigen Multithread-GUI-Designs, etwas anderes zu erfinden, über das ich viel einfacher nachdenken und die Art von Fehlern vermeiden konnte, die ich tendenziell machte, und einen der Gründe, warum ich es so viel einfacher fand, über das ich nachdenken konnte Am wenigsten liegt es an diesen homogenen Schleifen und daran, wie sie dem Kontrollfluss ähnelten, als ich in den DOS-Tagen abgefragt habe (obwohl es nicht wirklich abgefragt wird und nur dann Arbeit leistet, 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 isolieren und vereinheitlichen von Nebenwirkungen auf eine Weise, die es einfacher machte, sich nur auf das zu konzentrieren, was
quelle
LayoutSystem
der normalerweise schläft, aber wenn der Benutzer die Größe eines Steuerelements ändert, würde er eine Bedingungsvariable verwenden, um das Steuerelement aufzuweckenLayoutSystem
. DannLayoutSystem
ändert die Größe aller erforderlichen Steuerelemente und geht wieder in den Ruhezustand. Dabei werden die rechteckigen Bereiche, in denen sich die Widgets befinden, als aktualisierungsbedürftig markiert. Zu diesem Zeitpunkt wird APaintSystem
aktiviert und durchläuft diese rechteckigen Bereiche. Dabei werden die neu gezeichnet, die in einer flachen sequenziellen Schleife neu gezeichnet werden müssen.Ich gebe Ihnen einen Überblick über das konzeptionelle Denken über das Betrachtermuster. Denken Sie an ein Szenario wie das Abonnieren eines YouTube-Kanals. Es gibt eine Anzahl von Benutzern, die den Kanal abonnieren, und sobald der Kanal aktualisiert wird, der aus vielen Videos besteht, wird der Abonnent benachrichtigt, dass in diesem bestimmten Kanal eine Änderung vorliegt. Wir sind daher zu dem Schluss gekommen, dass, wenn der Kanal SUBJECT ist, der die Möglichkeit hat, sich anzumelden, alle im Kanal registrierten BEOBACHTER abgemeldet und benachrichtigt werden.
quelle