Wie kann man sich von einem Zusammenbruch einer endlichen Maschine erholen?


13

Meine Frage mag sehr wissenschaftlich erscheinen, aber ich denke, es ist ein häufiges Problem, und erfahrene Entwickler und Programmierer werden hoffentlich einen Rat haben, um das im Titel erwähnte Problem zu vermeiden. Übrigens ist das, was ich unten beschreibe, ein echtes Problem, das ich in meinem iOS-Projekt proaktiv zu lösen versuche. Ich möchte es auf jeden Fall vermeiden.

Mit Zustandsautomat meine ich Folgendes:> Ich habe eine Benutzeroberfläche mit einigen Schaltflächen, mehreren Sitzungszuständen, die für diese Benutzeroberfläche relevant sind, und was diese Benutzeroberfläche darstellt. Ich habe einige Daten, deren Werte teilweise in der Benutzeroberfläche angezeigt werden. Ich empfange und behandle einige externe Trigger (dargestellt durch Rückrufe von Sensoren). Ich habe Zustandsdiagramme erstellt, um die relevanten Szenarien, die in dieser Benutzeroberfläche und Anwendung wünschenswert und zulässig sind, besser abzubilden. Während ich den Code langsam implementiere, verhält sich die App immer mehr so, wie es sein sollte. Ich bin jedoch nicht sehr zuversichtlich, dass es robust genug ist. Meine Zweifel kommen von der Beobachtung meines eigenen Denk- und Umsetzungsprozesses. Ich war zuversichtlich, dass ich alles abgedeckt hatte, aber es war genug, um ein paar rohe Tests in der Benutzeroberfläche durchzuführen, und ich erkannte schnell, dass es immer noch Lücken im Verhalten gibt. Ich habe sie geflickt. Jedoch, Da jede Komponente von der Eingabe einer anderen Komponente abhängt und sich entsprechend verhält, löst eine bestimmte Eingabe eines Benutzers oder einer externen Quelle eine Kette von Ereignissen, Statusänderungen usw. aus. Ich habe mehrere Komponenten und jedes verhält sich so. Trigger empfangen am Eingang -> Trigger und sein Absender analysiert -> etwas (eine Nachricht, eine Zustandsänderung) basierend auf der Analyse ausgeben

Das Problem ist, dass dies nicht vollständig eigenständig ist und dass meine Komponenten (ein Datenbankelement, ein Sitzungsstatus, der Status einiger Schaltflächen) außerhalb des Bereichs der Ereigniskette oder nicht geändert, beeinflusst, gelöscht oder anderweitig modifiziert werden KÖNNTEN wünschenswertes Szenario. (Das Telefon stürzt ab, der Akku ist leer. Das Telefon schaltet sich plötzlich aus.) Dies führt zu einer ungültigen Situation im System, von der das System möglicherweise NICHT mehr wiederhergestellt werden kann. Ich sehe dies (obwohl die Leute nicht erkennen, dass dies das Problem ist) in vielen Apps meiner Konkurrenten, die im Apple Store erhältlich sind. Kunden schreiben solche Dinge> "Ich habe drei Dokumente hinzugefügt, und nachdem ich dorthin gegangen bin, kann ich sie nicht öffnen. auch wenn man sie sieht. " oder "Ich habe jeden Tag Videos aufgenommen, aber nachdem ich ein zu protokolliertes Video aufgenommen habe, kann ich keine Untertitel mehr deaktivieren. Und die Schaltfläche für Untertitel funktioniert nicht."

Dies sind nur verkürzte Beispiele, Kunden beschreiben sie häufig detaillierter. Aufgrund der in ihnen beschriebenen Beschreibungen und Verhaltensweisen gehe ich davon aus, dass die jeweilige App eine FSM-Aufschlüsselung aufweist.

Die ultimative Frage ist also, wie ich das vermeiden kann und wie man das System davor schützt, sich selbst zu blockieren.

BEARBEITEN> Ich spreche im Kontext der Ansicht eines Viewcontrollers am Telefon, ich meine einen Teil der Anwendung. Ich verstehe das MVC-Muster. Ich habe separate Module für unterschiedliche Funktionen. Alles, was ich beschreibe, ist für einen Canvas auf der Benutzeroberfläche relevant.


2
Klingt wie ein Fall für Unit-Tests!
Michael K

Antworten:


7

Ich bin sicher, dass Sie dies bereits wissen, aber nur für den Fall:

  1. Stellen Sie sicher, dass jeder Knoten im Zustandsdiagramm für JEDE zulässige Art von Eingabe einen ausgehenden Bogen aufweist (oder unterteilen Sie die Eingaben in Klassen, wobei für jede Eingabeklasse ein ausgehender Bogen vorhanden ist).

    Jedes Beispiel, das ich von einer Zustandsmaschine gesehen habe, verwendet nur einen ausgehenden Bogen für JEDE fehlerhafte Eingabe.

    Wenn es keine eindeutige Antwort gibt, was ein Eingang jedes Mal tun wird, liegt entweder eine Fehlerbedingung vor oder es fehlt ein anderer Eingang (dies sollte konsistent zu einem Lichtbogen für die Eingänge führen, die zu einem neuen Knoten gehen).

    Wenn ein Knoten keinen Lichtbogen für einen Eingabetyp hat, ist dies eine Annahme, dass die Eingabe im realen Leben niemals erfolgt (dies ist ein potenzieller Fehlerzustand, der von der Zustandsmaschine nicht behandelt werden würde).

  2. Stellen Sie sicher, dass die Zustandsmaschine nur EINEN Lichtbogen als Antwort auf die empfangene Eingabe (nicht mehr als einen Lichtbogen) aufnehmen oder verfolgen kann.

    Wenn es verschiedene Arten von Fehlerszenarien oder Eingaben gibt, die zur Entwurfszeit der Zustandsmaschine nicht identifiziert werden können, sollten die Fehlerszenarien und unbekannten Eingaben in einen von den "normalen Bögen" getrennten Zustand übergehen.

    Wenn ein Fehler oder eine Unbekannte in einem "bekannten" Zustand empfangen wird, sollten die als Ergebnis der Eingabe "Fehlerbehandlung / Unbekannt" verfolgten Bögen nicht in einen Zustand zurückkehren, in dem sich die Maschine befinden würde, wenn sie nur bekannte Eingaben empfangen hätte.

  3. Sobald Sie einen Terminal- (End-) Zustand erreicht haben, sollten Sie nicht in der Lage sein, zu einem Nicht-Terminal-Zustand nur zu dem einen Start- (Anfangs-) Zustand zurückzukehren.

  4. Für eine Zustandsmaschine sollte es nicht mehr als einen Start- oder Anfangszustand geben (basierend auf den Beispielen, die ich gesehen habe).

  5. Basierend auf dem, was ich gesehen habe, kann eine Zustandsmaschine nur den Zustand eines Problems oder Szenarios darstellen.
    Es sollte niemals mehrere mögliche Zustände gleichzeitig in einem Zustandsdiagramm geben.
    Wenn ich das Potenzial für mehrere gleichzeitige Zustände sehe, muss ich das Zustandsdiagramm in zwei oder mehr separate Zustandsautomaten aufteilen, die das Potenzial haben, dass jeder Zustand unabhängig geändert werden kann.


9

Der Sinn einer Zustandsmaschine ist, dass sie explizite Regeln für alles hat, was in einem Zustand passieren kann. Deshalb ist es endlich .

Zum Beispiel:

if a:
  print a
elif b:
  print b

Ist nicht endlich, weil wir Input bekommen könnten c. Dies:

if a:
  print a
elif b:
  print b
else:
  print error

ist endlich, weil alle möglichen Eingaben berücksichtigt werden. Dies berücksichtigt die möglichen Eingaben in einen Zustand , der von der Fehlerprüfung getrennt sein kann. Stellen Sie sich eine Zustandsmaschine mit den folgenden Zuständen vor:

No money state. 
Not enough money state.
Pick soda state.

Innerhalb der definierten Zustände werden alle möglichen Eingaben für eingezahltes Geld und ausgewähltes Soda behandelt. Der Stromausfall liegt außerhalb des Zustandsautomaten und "existiert nicht". Eine Zustandsmaschine kann nur Eingaben für Zustände verarbeiten, die sie hat. Sie haben also zwei Möglichkeiten.

  1. Stellen Sie sicher, dass alles atomar ist. Die Maschine kann einen totalen Stromausfall haben und dennoch alles in einem stabilen, korrekten Zustand belassen.
  2. Erweitern Sie Ihre Zustände, um unbekannte Probleme einzuschließen, und lassen Sie sich durch Fehler in diesen Zustand versetzen, in dem Probleme behandelt werden.

Als Referenz ist der Wiki-Artikel über Zustandsautomaten gründlich. Ich empfehle auch Code Complete für die Kapitel über die Erstellung stabiler, zuverlässiger Software.


"Wir könnten Eingabe c bekommen" - deshalb sind typsichere Sprachen so wichtig. Wenn Ihr Eingabetyp ein Bool ist, können Sie trueund erhalten false, aber sonst nichts. Auch dann ist es wichtig, Ihre Typen zu verstehen - z. B. nullfähige Typen, Gleitkommazahlen NaNusw.
MSalters

5

Reguläre Ausdrücke werden als Zustandsmaschinen implementiert. Die vom Bibliothekscode generierte Übergangstabelle hat einen eingebauten Fehlerstatus, um zu behandeln, was passiert, wenn die Eingabe nicht mit dem Muster übereinstimmt. Es gibt zumindest einen impliziten Übergang von fast jedem anderen Zustand in den Fehlerzustand.

Programmiersprachengrammatiken sind keine FSMs, aber die Parsergeneratoren (wie Yacc oder Bison) haben im Allgemeinen die Möglichkeit, einen oder mehrere Fehlerzustände festzulegen, sodass unerwartete Eingaben dazu führen können, dass der generierte Code in einen Fehlerzustand übergeht.

Klingt so, als ob Ihr FSM einen Fehlerzustand oder einen Fehlerzustand oder das moralische Äquivalent benötigt, zusammen mit expliziten (für von Ihnen erwartete Fälle) und impliziten (für von Ihnen nicht erwartete Fälle) Übergängen zu einem der Fehlerzustände.


Verzeihen Sie mir, wenn meine Frage albern klingt, weil ich keine formale Ausbildung in CS habe und gerade ein paar Monate Programmieren lerne. Bedeutet das, dass ich, wenn ich eine Handlermethode für ein Push-Ereignis für eine Schaltfläche gesagt habe, und in dieser Methode eine mäßig komplizierte If-else-Switch-Konditionierungsstruktur (20-30 Codezeilen) habe? Sollte ich unerwünschte eingaben immer explizit behandeln? ODER meinst du es auf "globaler" Ebene? Sollte ich eine separate Klasse haben, die diese FSM überwacht, und wenn das Problem auftritt, werden die Werte und Zustände zurückgesetzt?
Earl Grey

Die Erklärung von durch Yacc oder Bison generierten Parsern ist mir ein Rätsel. In der Regel werden jedoch bekannte Fälle behandelt und anschließend ein kleiner Codeblock für "alles andere geht in den Fehler- oder Fehlerzustand über". Der Code für den Fehler- / Fehlerstatus würde alles zurücksetzen. Möglicherweise muss ein zusätzlicher Wert angegeben werden, der angibt, warum Sie in den Fehlerzustand versetzt wurden.
Bruce Ediger

Ihr FSM sollte mindestens einen Fehlerstatus oder mehrere Fehlerstatus für verschiedene Arten von Fehlern haben.
Whatsisname

3

Der beste Weg, dies zu vermeiden, ist das automatisierte Testen .

Die einzige Möglichkeit, wirklich Vertrauen in die Funktionsweise Ihres Codes auf der Grundlage bestimmter Eingaben zu haben, besteht darin, sie zu testen. Sie können in Ihrer Anwendung herumklicken und versuchen, Dinge falsch zu machen, aber das lässt sich nicht gut skalieren, um sicherzustellen, dass Sie keine Regressionen haben. Stattdessen können Sie einen Test erstellen, der fehlerhafte Eingaben in eine Codekomponente übergibt und sicherstellt, dass diese ordnungsgemäß verarbeitet wird.

Dies wird nicht beweisen können, dass die Zustandsmaschine niemals kaputt gehen kann, aber es wird Ihnen sagen, dass viele der üblichen Fälle korrekt gehandhabt werden und andere Dinge nicht kaputt gehen.


2

Was Sie suchen, ist die Ausnahmebehandlung. Eine Designphilosophie, die es vermeidet, in einem inkonsistenten Zustand zu bleiben, wird wie folgt dokumentiert the way of the samurai: Rückkehr als Sieger oder Nichtrückkehr. Mit anderen Worten: Eine Komponente sollte alle Eingaben überprüfen und sicherstellen, dass sie diese normal verarbeiten kann. Ist dies nicht der Fall, sollte eine Ausnahme mit nützlichen Informationen ausgelöst werden.

Sobald eine Ausnahme ausgelöst wird, wird der Stapel gesprengt. Sie sollten eine Fehlerbehandlungsebene definieren, die weiß, was zu tun ist. Wenn eine Benutzerdatei beschädigt ist, erklären Sie Ihrem Client, dass die Daten verloren gegangen sind, und erstellen Sie eine leere saubere Datei neu.

Der wichtige Teil hierbei ist, wieder in einen Arbeitszustand zurückzukehren und eine Fehlerausbreitung zu vermeiden. Wenn Sie damit fertig sind, können Sie einzelne Komponenten bearbeiten, um sie robuster zu machen.

Ich bin kein Objective-C-Experte, aber diese Seite sollte ein guter Ausgangspunkt sein:

http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocExceptionHandling.html


1

Vergiss deine Finite-State-Maschine. Was Sie hier haben, ist eine ernste Multithreading- Situation. Jeder Knopf kann jederzeit gedrückt werden und Ihre externen Trigger können jederzeit ausgelöst werden. Die Schaltflächen befinden sich wahrscheinlich alle auf einem Thread, aber ein Trigger kann zeitgleich mit einer der Schaltflächen oder einem, mehreren oder allen anderen Triggern ausgelöst werden.

Was Sie tun müssen, ist, Ihren Zustand zu bestimmen, sobald Sie sich entscheiden, zu handeln. Holen Sie sich alle Schaltflächen und Trigger-Zustände. Speichern Sie sie in lokalen Variablen. Die ursprünglichen Werte können sich jedes Mal ändern, wenn Sie sie ansehen. Dann handeln Sie so, wie Sie es haben. Es ist eine Momentaufnahme davon, wie das System an einem Punkt aussah. Eine Millisekunde später könnte es ganz anders aussehen, aber mit Multithreading gibt es kein echtes "Jetzt", an dem Sie sich festhalten können, sondern nur ein Bild, das Sie in lokalen Variablen gespeichert haben.

Dann müssen Sie auf Ihren gespeicherten - historischen - Zustand reagieren. Alles ist fest und Sie sollten eine Aktion für alle möglichen Zustände haben. Es werden keine Änderungen berücksichtigt, die zwischen der Erstellung des Snapshots und der Anzeige Ihrer Ergebnisse vorgenommen wurden, aber das ist das Leben in der Multithreading-Welt. Möglicherweise müssen Sie die Synchronisierung verwenden, um zu verhindern, dass der Schnappschuss zu unscharf wird. (Sie können nicht auf dem neuesten Stand sein, aber Sie können fast Ihren gesamten Zustand von einem bestimmten Zeitpunkt abrufen.)

Informieren Sie sich über Multithreading. Sie haben eine Menge zu lernen. Und aufgrund dieser Trigger können Sie wahrscheinlich nicht viele der häufig bereitgestellten Tricks verwenden, um die parallele Verarbeitung zu vereinfachen ("Arbeitsthreads" usw.). Sie führen keine "Parallelverarbeitung" durch. Sie versuchen nicht, 75% der 8 Kerne zu verwenden. Sie verbrauchen 1% der gesamten CPU, haben jedoch sehr unabhängige, stark interagierende Threads, und es sind viele Überlegungen erforderlich, um sie zu synchronisieren und zu verhindern, dass die Synchronisierung das System blockiert.

Test auf Einkern- und Mehrkernmaschinen; Ich habe festgestellt, dass sie sich beim Multithreading etwas anders verhalten. Auf den Single-Core-Rechnern treten weniger Multithreading-Fehler auf, aber diese Fehler sind weitaus seltsamer. (Obwohl die Multi-Core-Maschinen Ihre Meinung verwirren werden, bis Sie sich an sie gewöhnen.)

Ein letzter unangenehmer Gedanke: Das ist nicht einfach zu testen. Sie müssen zufällige Auslöser und Tastendrücke generieren und das System für eine Weile auf Hochtouren laufen lassen, um zu sehen, was sich herausstellt. Multithread-Code ist nicht deterministisch. Einmal in einer Milliarde Durchläufen kann etwas schief gehen, nur weil das Timing für eine Nanosekunde verschoben war. Geben Sie Debug-Anweisungen ein (mit sorgfältigen if-Anweisungen, um 999.999.999 nicht benötigte Nachrichten zu vermeiden), und Sie müssen eine Milliarde Durchläufe ausführen, nur um eine nützliche Nachricht zu erhalten. Glücklicherweise sind Maschinen heutzutage sehr schnell.

Tut mir leid, das alles so früh in Ihrer Karriere auf Sie zu werfen. Hoffentlich findet jemand eine andere Antwort mit einem Weg, um all das zu umgehen (ich denke, es gibt Dinge, die die Auslöser zähmen könnten, aber Sie haben immer noch den Konflikt zwischen Auslöser und Schaltfläche). Wenn ja, wird diese Antwort Ihnen zumindest mitteilen, was Sie vermissen. Viel Glück.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.