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:
"wie das Verwenden von Bedingungsvariablen, um Threads zum Aufwecken zu benachrichtigen" Klingt wie eine ereignisbasierte / beobachtende Anordnung, keine Abfrage
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 die PaintSystem
Aufgabe zu warnen, gibt jedoch nicht mehr als das anPaintSystem
widmet sich zu diesem Zeitpunkt einer einheitlichen, sehr homogenen Aufgabe. Wenn wir den PaintSystem'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:
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 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:
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 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