Haftungsausschluss: Dieser Artikel beschreibt eine nicht offensichtliche Lösung für ein nicht offensichtliches Problem. Vor dem Rauschen Eier Um es in die Praxis umzusetzen, empfehle ich, den Artikel bis zum Ende zu lesen und zweimal darüber nachzudenken.

Hallo allerseits! Wenn wir mit Code arbeiten, müssen wir uns oft mit dem Status befassen. Ein solcher Fall ist der Lebenszyklus von Objekten. Das Verwalten eines Objekts mit mehreren möglichen Zuständen kann eine sehr nicht triviale Aufgabe sein. Fügen Sie hier eine asynchrone Ausführung hinzu, und die Aufgabe wird um eine Größenordnung kompliziert. Es gibt eine effektive und natürliche Lösung. In diesem Artikel werde ich über die Ereignismaschine und deren Implementierung in Go sprechen.
Warum den Staat verwalten?
Lassen Sie uns zunächst das Konzept selbst definieren. Das einfachste Beispiel für einen Status: Dateien und verschiedene Verbindungen. Sie können nicht einfach eine Datei nehmen und lesen. Es muss zuerst geöffnet werden und am Ende vorzugsweise Achten Sie darauf, zu schließen. Es stellt sich heraus, dass die aktuelle Aktion vom Ergebnis der vorherigen Aktion abhängt: Das Lesen hängt von der Öffnung ab. Das gespeicherte Ergebnis ist der Status.
Das Hauptproblem mit dem Staat ist die Komplexität. Jeder Status verkompliziert den Code automatisch. Sie müssen die Ergebnisse von Aktionen im Speicher speichern und der Logik verschiedene Prüfungen hinzufügen. Deshalb sind zustandslose Architekturen für Programmierer so attraktiv - niemand will Ärger Schwierigkeiten. Wenn die Ergebnisse Ihrer Aktionen keinen Einfluss auf die Ausführungslogik haben, benötigen Sie keinen Status.
Es gibt jedoch eine Eigenschaft, mit der Sie mit den Schwierigkeiten rechnen müssen. Für einen Status müssen Sie eine bestimmte Reihenfolge von Aktionen einhalten. Im Allgemeinen sollten solche Situationen vermieden werden, dies ist jedoch nicht immer möglich. Ein Beispiel ist der Lebenszyklus von Programmobjekten. Dank eines guten Zustandsmanagements kann ein vorhersagbares Verhalten von Objekten mit einem komplexen Lebenszyklus erzielt werden.
Lassen Sie uns nun herausfinden, wie man es cool macht .
Automatisch, um Probleme zu lösen

Wenn Menschen über Zustände sprechen, fallen ihnen sofort endliche Zustandsmaschinen ein. Dies ist logisch, da ein Automat die natürlichste Art ist, einen Zustand zu verwalten.
Ich werde mich nicht mit der Theorie der Automaten befassen , da es im Internet mehr als genug Informationen gibt.
Wenn Sie nach Beispielen für Finite-State-Maschinen für Go suchen, werden Sie auf jeden Fall einen Lexer von Rob Pike treffen. Ein gutes Beispiel für einen Automaten, bei dem die verarbeiteten Daten das Eingabealphabet sind. Dies bedeutet, dass Zustandsübergänge durch den vom Lexer verarbeiteten Text verursacht werden. Elegante Lösung für ein bestimmtes Problem.
Das Wichtigste zu verstehen ist, dass ein Automat eine Lösung für ein streng spezifisches Problem ist. Bevor Sie es als Abhilfe für alle Probleme betrachten, müssen Sie die Aufgabe daher vollständig verstehen. Insbesondere die Entität, die Sie steuern möchten:
- Zustände - Lebenszyklus;
- Ereignisse - was genau bewirkt den Übergang in jeden Zustand;
- Arbeitsergebnis - Ausgabedaten;
- Ausführungsmodus (synchron / asynchron);
- Hauptanwendungsfälle.
Der Lexer ist wunderschön, ändert jedoch nur den Status aufgrund von Daten, die er selbst verarbeitet. Aber was ist mit der Situation, wenn der Benutzer Übergänge aufruft? Hier kann die Eventmaschine helfen.
Echtes Beispiel
Um es klarer zu machen, werde ich ein Beispiel aus der phono
analysieren.
Um vollständig in den Kontext einzutauchen, können Sie den einleitenden Artikel lesen. Dies ist für dieses Thema nicht erforderlich, hilft jedoch dabei, besser zu verstehen, was wir verwalten.
Und was verwalten wir?
phono
basiert auf der DSP-Pipeline. Es besteht aus drei Verarbeitungsstufen. Jede Stufe kann eine bis mehrere Komponenten umfassen:

pipe.Pump
(englische Pumpe) ist eine obligatorische Stufe für den pipe.Pump
, immer nur eine Komponente.pipe.Processor
(englischer Handler) - eine optionale Stufe der Klangverarbeitung von 0 bis N Komponenten.pipe.Sink
(englische Spüle) - eine obligatorische Stufe der Schallübertragung von 1 bis N Komponenten.
Eigentlich werden wir den Fördererlebenszyklus verwalten.
Lebenszyklus
So sieht das pipe.Pipe
Zustandsdiagramm aus.

Kursivschrift kennzeichnet Übergänge, die durch die interne Ausführungslogik verursacht werden. Fett - Übergänge, die durch Ereignisse verursacht werden. Das Diagramm zeigt, dass die Zustände in zwei Typen unterteilt sind:
- Ruhezustände -
ready
und paused
, Sie können nur nach Ereignis von ihnen springen - aktive Zustände -
running
und pausing
, Übergänge nach Ereignis und aufgrund der Ausführungslogik
Vor einer detaillierten Analyse des Codes ein klares Beispiel für die Verwendung aller Zustände:
Nun, das Wichtigste zuerst.
Der gesamte Quellcode ist im Repository verfügbar.
Staaten und Ereignisse
Beginnen wir mit dem Wichtigsten.
Dank separater Typen werden Übergänge auch für jeden Status separat deklariert. Dies vermeidet das riesige Würstchen Übergangsfunktionen mit verschachtelten switch
. Die Zustände selbst enthalten keine Daten oder Logik. Für sie können Sie Variablen auf Paketebene deklarieren, um dies nicht jedes Mal zu tun. Die state
wird für den Polymorphismus benötigt. activeState
idleState
etwas später über activeState
und idleState
sprechen.
Der zweitwichtigste Teil unserer Maschine sind Ereignisse.
Betrachten Sie ein einfaches Beispiel, um zu verstehen, warum der target
benötigt wird. Wir haben ein neues Förderband geschaffen, es ist ready
. Führen Sie es nun mit p.Run()
. Das run
wird an die Maschine gesendet, die Pipeline geht in den running
. Wie kann man herausfinden, wann der Förderer fertig ist? Hier hilft uns der Zieltyp. Es zeigt an, welcher Ruhezustand nach dem Ereignis zu erwarten ist. In unserem Beispiel wird die Pipeline nach Abschluss der Arbeiten wieder in den ready
. Das gleiche im Diagramm:

Nun mehr zu den Arten von Staaten. Genauer gesagt über die activeState
idleState
und activeState
. Schauen wir uns die listen(*Pipe, target) (state, target)
für verschiedene Arten von Stufen an:
pipe.Pipe
hat verschiedene Funktionen, um auf einen Übergang zu warten! Was ist dort?
Somit können wir verschiedene Kanäle in verschiedenen Zuständen hören. Auf diese Weise können Sie beispielsweise während einer Pause keine Nachrichten senden. Wir hören nur nicht auf den entsprechenden Kanal.
Konstruktor und Startmaschine

Neben der Initialisierung und den Funktionsoptionen beginnt eine separate Goroutine mit dem Hauptzyklus. Schau ihn dir an:
Der Förderer wird im Vorgriff auf Ereignisse erstellt und eingefroren.
Zeit zu arbeiten
Rufen Sie p.Run()
!

running
generiert Nachrichten und wird ausgeführt, bis die Pipeline abgeschlossen ist.
Pause
Während der Ausführung des Förderers können wir ihn anhalten. In diesem Zustand generiert die Pipeline keine neuen Nachrichten. Rufen Sie dazu die Methode p.Pause()
.

Sobald alle Empfänger die Nachricht erhalten, wechselt die Pipeline paused
Zustand. Wenn die Nachricht die letzte ist, erfolgt der Übergang in den ready
.
Zurück zur Arbeit!
Rufen Sie p.Resume()
, um den paused
Status zu p.Resume()
.

Hier ist alles trivial, die Pipeline geht wieder in running
.
Machen Sie es sich bequem
Der Förderer kann aus jedem Zustand angehalten werden. Dafür gibt es p.Close()
.

Wer braucht das?
Nicht für jedermann. Um genau zu verstehen, wie der Status verwaltet wird, müssen Sie Ihre Aufgabe verstehen. Es gibt genau zwei Umstände, unter denen Sie eine ereignisbasierte asynchrone Maschine verwenden können:
- Komplexer Lebenszyklus - Es gibt drei oder mehr Zustände mit nichtlinearen Übergängen.
- Es wird eine asynchrone Ausführung verwendet.
Obwohl die Ereignismaschine das Problem löst, ist es ein ziemlich kompliziertes Muster. Daher sollte es mit großer Sorgfalt und nur nach einem vollständigen Verständnis aller Vor- und Nachteile verwendet werden.
Referenzen