In diesem Artikel lernen Sie die Grundlagen der Parallelität in Elixir kennen und erfahren, wie Sie Prozesse erzeugen, Nachrichten senden und empfangen und langwierige Prozesse erstellen. Außerdem lernen Sie GenServer kennen, sehen, wie es in Ihrer Anwendung verwendet werden kann, und entdecken Sie einige nützliche Dinge, die es für Sie bereitstellt.
Wie Sie wahrscheinlich wissen, ist Elixir eine funktionale Sprache, mit der fehlertolerante, gleichzeitige Systeme erstellt werden, die viele gleichzeitige Anforderungen verarbeiten. BEAM (Erlang Virtual Machine) verwendet Prozesse um verschiedene Aufgaben gleichzeitig auszuführen, was bedeutet, dass zum Beispiel die Bearbeitung einer Anfrage keine andere blockiert. Prozesse sind leicht und isoliert, was bedeutet, dass sie keinen Speicher gemeinsam nutzen. Selbst wenn ein Prozess abstürzt, können andere weiterlaufen.
BEAM-Prozesse unterscheiden sich stark von den OS-Prozesse. Grundsätzlich läuft BEAM in einem OS-Prozess und verwendet einen eigenen Planer. Jeder Scheduler belegt einen CPU-Kern, läuft in einem separaten Thread und kann gleichzeitig Tausende von Prozessen verarbeiten (die abwechselnd ausgeführt werden). Weitere Informationen zu BEAM und zum Multithreading finden Sie in StackOverflow.
Wie Sie sehen, sind BEAM-Prozesse (von nun an nur noch "Prozesse") in Elixir sehr wichtig. Die Sprache bietet Ihnen einige einfache Tools zum manuellen Erzeugen von Prozessen, zum Verwalten des Status und zum Bearbeiten der Anforderungen. Nur wenige Leute benutzen sie - es ist üblicher, sich auf das Internet zu verlassen Open Telecom Platform (OTP) Rahmen dafür zu tun.
OTP hat heutzutage nichts mit Telefonen zu tun - es ist ein allgemeiner Rahmen für den Aufbau komplexer gleichzeitiger Systeme. Es definiert, wie Ihre Anwendungen strukturiert werden sollten, und bietet eine Datenbank sowie eine Reihe sehr nützlicher Tools zum Erstellen von Serverprozessen, zum Beheben von Fehlern, zum Durchführen von Protokollierungen usw. In diesem Artikel werden wir über a sprechen Serververhalten GenServer genannt, das von OTP bereitgestellt wird.
Sie können sich GenServer als eine Abstraktion oder einen Helfer vorstellen, der die Arbeit mit Serverprozessen vereinfacht. Zunächst erfahren Sie, wie Sie mit einfachen Funktionen Prozesse erzeugen können. Dann werden wir zu GenServer wechseln und sehen, wie es die Dinge für uns vereinfacht, indem wir nicht mehr langwierigen (und ziemlich generischen) Code schreiben müssen. Lass uns anfangen!
Wenn Sie mich gefragt haben, wie Sie einen Prozess in Elixir erstellen können, würde ich antworten: laichen es! Spawn / 1 ist eine Funktion innerhalb der Kernel
Modul, das einen neuen Prozess zurückgibt. Diese Funktion akzeptiert ein Lambda, das im erstellten Prozess ausgeführt wird. Sobald die Ausführung abgeschlossen ist, wird auch der Prozess beendet:
spawn (fn -> IO.puts ("hi") end) |> IO.inspect # => hi # => #PID<0.72.0>
Also hier laichen
gab eine neue Prozess-ID zurück. Wenn Sie dem Lambda eine Verzögerung hinzufügen, wird die Zeichenfolge "hi" nach einiger Zeit ausgedruckt:
spawn (fn ->: timer.sleep (5000) IO.puts ("hi") end) |> IO.inspect # => #PID<0.82.0> # => (nach 5 Sekunden) "hi"
Jetzt können wir beliebig viele Prozesse erzeugen, die gleichzeitig ausgeführt werden:
spawn_it = fn (num) -> spawn (fn ->: timer.sleep (5000) IO.puts ("hi # num") end) end Enum.each (1… 10, fn (_) -> spawn_it . (: rand.uniform (100)) end) # => (alle werden nach 5 Sekunden zur gleichen Zeit ausgedruckt) # => hi 5 # => hi 10 etc…
Hier erzeugen wir zehn Prozesse und drucken einen Teststring mit einer Zufallszahl aus. : rand
ist ein von Erlang bereitgestelltes Modul, daher ist der Name ein Atom. Cool ist, dass alle Nachrichten nach fünf Sekunden zur gleichen Zeit ausgedruckt werden. Dies geschieht, weil alle zehn Prozesse gleichzeitig ausgeführt werden.
Vergleichen Sie es mit dem folgenden Beispiel, das dieselbe Aufgabe ausführt, jedoch nicht verwendet Laich / 1
:
dont_spawn_it = fn (num) ->: timer.sleep (5000) IO.puts ("hi # num") end Enum.each (1… 10, fn (_) -> dont_spawn_it. (: rand.uniform ( 100)) end) # => (nach 5 Sekunden) hi 70 # => (nach weiteren 5 Sekunden) hi 45 # => etc…
Während dieser Code ausgeführt wird, können Sie in die Küche gehen und eine weitere Tasse Kaffee zubereiten, da dies fast eine Minute dauert. Jede Nachricht wird nacheinander angezeigt, was natürlich nicht optimal ist!
Sie könnten fragen: "Wie viel Speicher verbraucht ein Prozess?" Nun, das hängt davon ab, aber anfangs dauert es ein paar Kilobytes, was eine sehr kleine Anzahl ist (sogar mein alter Laptop hat 8 GB Speicher, ganz zu schweigen von coolen modernen Servern)..
So weit, ist es gut. Bevor wir jedoch mit GenServer arbeiten, wollen wir noch eine weitere wichtige Sache besprechen: das Passieren und Empfangen Mitteilungen.
Es ist keine Überraschung, dass Prozesse (die, wie Sie sich erinnern, isoliert sind, auf irgendeine Weise kommunizieren müssen, insbesondere wenn mehr oder weniger komplexe Systeme erstellt werden sollen). Um dies zu erreichen, können wir Nachrichten verwenden.
Eine Nachricht kann mit einer Funktion mit einem eindeutigen Namen gesendet werden: send / 2. Es akzeptiert ein Ziel (Port, Prozess-ID oder einen Prozessnamen) und die eigentliche Nachricht. Nachdem die Nachricht gesendet wurde, erscheint sie im Briefkasten eines Prozesses und kann verarbeitet werden. Wie Sie sehen, ähnelt die allgemeine Idee unserer täglichen Arbeit beim E-Mail-Austausch.
Eine Mailbox ist im Grunde eine FIFO-Warteschlange (First In First Out). Nachdem die Nachricht verarbeitet wurde, wird sie aus der Warteschlange entfernt. Um mit dem Empfangen von Nachrichten zu beginnen, müssen Sie raten, was! -A ein Makro empfangen. Dieses Makro enthält eine oder mehrere Klauseln, und eine Nachricht wird mit ihnen abgeglichen. Wenn eine Übereinstimmung gefunden wird, wird die Nachricht verarbeitet. Andernfalls wird die Nachricht wieder in die Mailbox eingefügt. Darüber hinaus können Sie eine Option festlegen nach dem
Klausel, die ausgeführt wird, wenn in der angegebenen Zeit keine Nachricht empfangen wurde. Sie können mehr darüber lesen senden / 2
und erhalten
in den offiziellen Dokumenten.
Okay, genug mit der Theorie. Versuchen wir, mit den Nachrichten zu arbeiten. Senden Sie zunächst etwas an den aktuellen Prozess:
senden (selbst (), "hallo!")
Das self / 0-Makro gibt eine PID des aufrufenden Prozesses zurück, was genau das ist, was wir brauchen. Lassen Sie nach der Funktion keine runden Klammern aus, da Sie eine Warnung bezüglich der Mehrdeutigkeit erhalten.
Empfangen Sie jetzt die Nachricht, während Sie einstellen nach dem
Klausel:
empfangen do msg -> IO.puts "Ja, eine Nachricht: # msg" msg nach 1000 -> IO.puts: stderr, "Ich möchte Nachrichten!" end |> IO.puts # => Ja, eine Nachricht: Hallo! # => hallo!
Beachten Sie, dass die Klausel das Ergebnis der Auswertung der letzten Zeile zurückgibt. Wir erhalten also das "Hallo!" Schnur.
Denken Sie daran, dass Sie beliebig viele Klauseln einführen können:
send (self (), : ok, "hallo!") receive do : ok, msg -> IO.puts "Ja, eine Nachricht: # msg" msg : error, msg -> IO .puts: stderr, "Oh nein, etwas Schlimmes ist passiert: # msg" _ -> IO.puts "Ich weiß nicht, was diese Nachricht ist ..." nach 1000 -> IO.puts: stderr, "Ich möchte Nachrichten!" end |> IO.puts
Hier haben wir vier Klauseln: eine, um eine Erfolgsmeldung zu behandeln, eine andere, um Fehler zu behandeln, und dann eine "Fallback" -Klausel und ein Timeout.
Wenn die Nachricht mit keiner der Klauseln übereinstimmt, wird sie in der Mailbox aufbewahrt, was nicht immer wünschenswert ist. Warum? Denn wenn eine neue Nachricht eingeht, werden die alten Nachrichten im ersten Kopf verarbeitet (da das Postfach eine FIFO-Warteschlange ist), wodurch das Programm abgebremst wird. Daher kann eine "Rückfall" -Klausel hilfreich sein.
Nachdem Sie nun wissen, wie Prozesse erzeugt, Nachrichten gesendet und empfangen werden, werfen wir einen Blick auf ein etwas komplexeres Beispiel, bei dem ein einfacher Server erstellt wird, der auf verschiedene Nachrichten reagiert.
Im vorherigen Beispiel haben wir nur eine Nachricht gesendet, sie erhalten und einige Arbeiten ausgeführt. Das ist in Ordnung, aber nicht sehr funktionell. Normalerweise haben wir einen Server, der auf verschiedene Nachrichten reagieren kann. Mit "Server" meine ich einen lang laufenden Prozess, der mit einer wiederkehrenden Funktion erstellt wurde. Lassen Sie uns beispielsweise einen Server erstellen, um einige mathematische Gleichungen auszuführen. Es wird eine Nachricht mit der angeforderten Operation und einigen Argumenten erhalten.
Beginnen Sie mit der Erstellung des Servers und der Schleifenfunktion:
defmodule MathServer def defule do launch & listen / 0 end defp listen do do erhalten : sqrt, caller, arg -> IO.puts arg _ -> IO.puts: stderr, "Nicht implementiert." Ende hören () Ende Ende
Wir erzeugen also einen Prozess, der die eingehenden Nachrichten ständig überwacht. Nachdem die Nachricht empfangen wurde, wird die hören / 0
Funktion wird erneut aufgerufen, wodurch eine Endlosschleife entsteht. In der hören / 0
Funktion fügen wir Unterstützung für die : sqrt
Nachricht, die die Quadratwurzel einer Zahl berechnet. Das arg
enthält die tatsächliche Nummer, für die die Operation ausgeführt werden soll. Außerdem definieren wir eine Rückfallklausel.
Sie können jetzt den Server starten und seine Prozess-ID einer Variablen zuweisen:
math_server = MathServer.start IO.inspect math_server # => #PID<0.85.0>
Brillant! Nun fügen wir ein Implementierungsfunktion um die Berechnung tatsächlich durchzuführen:
defmodule MathServer do #… def sqrt (server, arg) sendet (: some_name, : sqrt, self (), arg) end end
Nutzen Sie jetzt diese Funktion:
MathServer.sqrt (math_server, 3) # => 3
Im Moment wird das übergebene Argument einfach ausgedruckt. Passen Sie Ihren Code so an, um die mathematische Operation auszuführen:
defmodule MathServer do #… defp Listen erhalten do : sqrt, caller, arg -> send (: irgendein_Name, : Ergebnis, do_sqrt (arg)) _ -> IO.puts: stderr, "Nicht implementiert." end listen () end defp do_sqrt (arg) do: math.sqrt (arg) end end
Nun wird noch eine weitere Nachricht an den Server gesendet, der das Ergebnis der Berechnung enthält.
Interessant ist, dass die sqrt / 2
Die Funktion sendet einfach eine Nachricht an den Server, in der Sie aufgefordert werden, eine Operation auszuführen, ohne auf das Ergebnis zu warten. Im Grunde führt es also eine asynchroner Aufruf.
Natürlich möchten wir das Ergebnis zu einem bestimmten Zeitpunkt erfassen, also eine andere öffentliche Funktion programmieren:
def grab_result erhalten do : result, result -> result nach 5000 -> IO.puts: stderr, "timeout" end end
Jetzt nutzen Sie es:
math_server = MathServer.start MathServer.sqrt (math_server, 3) MathServer.grab_result |> IO.puts # => 1.7320508075688772
Es klappt! Natürlich können Sie sogar einen Pool von Servern erstellen und Aufgaben zwischen ihnen verteilen, um Parallelität zu erreichen. Es ist praktisch, wenn sich die Anforderungen nicht auf einander beziehen.
In Ordnung, wir haben eine Handvoll Funktionen abgedeckt, die es uns ermöglichen, langwierige Serverprozesse zu erstellen und Nachrichten zu senden und zu empfangen. Das ist großartig, aber wir müssen zu viel Boilerplate-Code schreiben, der eine Serverschleife startet (Start / 0
), antwortet auf Nachrichten (hören / 0
private Funktion) und gibt ein Ergebnis zurück (grab_result / 0
). In komplexeren Situationen müssen wir möglicherweise auch einen gemeinsamen Zustand verwalten oder die Fehler behandeln.
Wie ich zu Beginn des Artikels gesagt habe, besteht keine Notwendigkeit, ein Fahrrad neu zu erfinden. Stattdessen können wir das Verhalten von GenServer nutzen, das bereits den gesamten Boilerplate-Code bereitstellt und die Serverprozesse hervorragend unterstützt (wie wir im vorherigen Abschnitt gesehen haben)..
Verhalten In Elixir ist ein Code, der ein gemeinsames Muster implementiert. Um GenServer verwenden zu können, müssen Sie ein spezielles definieren Rückrufmodul das erfüllt den durch das Verhalten vorgegebenen Vertrag. Insbesondere sollten einige Callback-Funktionen implementiert werden, und die tatsächliche Implementierung liegt bei Ihnen. Nachdem die Rückrufe geschrieben wurden, wird die Verhaltensmodul kann sie verwenden.
Wie in den Dokumenten angegeben, erfordert GenServer die Implementierung von sechs Callbacks, obwohl sie auch eine Standardimplementierung haben. Dies bedeutet, dass Sie nur diejenigen neu definieren können, für die benutzerdefinierte Logik erforderlich ist.
Das Wichtigste zuerst: Wir müssen den Server starten, bevor Sie etwas anderes tun. Fahren Sie mit dem nächsten Abschnitt fort!
Um die Verwendung von GenServer zu demonstrieren, schreiben Sie a CalcServer
Auf diese Weise können Benutzer verschiedene Operationen auf ein Argument anwenden. Das Ergebnis der Operation wird in a gespeichert Serverzustand, und dann kann auch eine andere Operation darauf angewendet werden. Oder ein Benutzer kann ein Endergebnis der Berechnungen erhalten.
Verwenden Sie zunächst das Use-Makro, um GenServer einzufügen:
Defmodule CalcServer verwenden GenServer-Ende
Nun müssen wir einige Rückrufe neu definieren.
Die erste ist init / 1, die beim Start eines Servers aufgerufen wird. Das übergebene Argument wird verwendet, um den anfänglichen Status des Servers festzulegen. Im einfachsten Fall sollte dieser Rückruf die : ok, Anfangszustand
Tupel, es gibt jedoch auch andere mögliche Rückgabewerte wie : Stop, Grund
, Dadurch wird der Server sofort angehalten.
Ich denke, wir können Benutzern erlauben, den Anfangszustand für unseren Server zu definieren. Wir müssen jedoch überprüfen, dass das übergebene Argument eine Zahl ist. Verwenden Sie also eine Guard-Klausel:
defmodule CalcServer verwendet GenServer def init (initial_value), wenn is_number (initial_value) do : ok, initial_value end def init (_) do : stop, "Der Wert muss eine Ganzzahl sein!" end end
Starten Sie nun einfach den Server mit der start / 3-Funktion und geben Sie Ihren Server an CalcServer
als Rückrufmodul (erstes Argument). Das zweite Argument ist der Anfangszustand:
GenServer.start (CalcServer, 5.1) |> IO.inspect # => : ok, #PID<0.85.0>
Wenn Sie versuchen, eine zweite Nummer als zweites Argument zu übergeben, wird der Server nicht gestartet. Genau das ist es, was wir brauchen.
Großartig! Nun, da unser Server läuft, können wir damit beginnen, mathematische Operationen zu codieren.
Asynchrone Anforderungen werden aufgerufen Abgüsse in GenServers Bedingungen. Um eine solche Anfrage auszuführen, verwenden Sie die Funktion cast / 2, die einen Server und die tatsächliche Anfrage akzeptiert. Es ist dem ähnlich sqrt / 2
Funktion, die wir codiert haben, wenn wir über Serverprozesse sprechen. Es verwendet auch den Ansatz "Feuer und Vergessen", was bedeutet, dass wir nicht warten, bis die Anfrage abgeschlossen ist.
Für die Verarbeitung der asynchronen Nachrichten wird ein handle_cast / 2-Callback verwendet. Es akzeptiert eine Anfrage und einen Status und sollte mit einem Tupel antworten : noreply, new_state
im einfachsten Fall (oder : stop, reason, new_state
um die Server-Schleife zu stoppen). Lassen Sie uns zum Beispiel mit einem Asynchronen umgehen : sqrt
Besetzung:
def handle_cast (: sqrt, state) do : noreply,: math.sqrt (state) ende
So behalten wir den Status unseres Servers bei. Anfangs war die Nummer (die beim Start des Servers übergeben wurde) 5.1
. Jetzt aktualisieren wir den Zustand und setzen ihn auf : math.sqrt (5.1)
.
Codieren Sie die Schnittstellenfunktion, die verwendet wird Besetzung / 2
:
def sqrt (pid) GenServer.cast (pid,: sqrt) beenden
Für mich ähnelt dies einem bösen Zauberer, der einen Zauber wirkt, sich aber nicht um die Auswirkungen kümmert, die er verursacht.
Beachten Sie, dass wir eine Prozess-ID benötigen, um die Umwandlung durchzuführen. Denken Sie daran, dass, wenn ein Server erfolgreich gestartet wird, ein Tupel : ok, pid
ist zurück gekommen. Verwenden wir daher Mustervergleich, um die Prozess-ID zu extrahieren:
: ok, pid = GenServer.start (CalcServer, 5.1) CalcServer.sqrt (pid)
Nett! Der gleiche Ansatz kann zum Implementieren einer Multiplikation verwendet werden. Der Code wird etwas komplexer, da wir das zweite Argument, einen Multiplikator, übergeben müssen:
def Multiplikation (PID, Multiplikator) GenServer.cast (PID, : Multiplikator, Multiplikator) beenden
Das Besetzung
Die Funktion unterstützt nur zwei Argumente, daher muss ich ein Tupel erstellen und dort ein zusätzliches Argument übergeben.
Nun zum Rückruf:
def handle_cast (: multiplizieren, multiplikator, state) do : noreply, state * multiplier ende
Wir können auch eine Single schreiben handle_cast
Rückruf, der den Betrieb sowie das Stoppen des Servers unterstützt, wenn der Vorgang unbekannt ist:
def handle_cast (operation, state) do case operation do: sqrt -> : noreply,: math.sqrt (state) : multiplizieren, multiplikator -> : noreply, state * multiplier _ -> : stop, "Nicht implementiert", Zustand Ende Ende
Verwenden Sie nun die neue Schnittstellenfunktion:
CalcServer.multiply (pid, 2)
Großartig, aber derzeit gibt es keine Möglichkeit, ein Ergebnis der Berechnungen zu erhalten. Daher ist es an der Zeit, einen weiteren Rückruf zu definieren.
Wenn asynchrone Anforderungen Umsetzungen sind, werden synchrone Anforderungen benannt Anrufe. Verwenden Sie zum Ausführen solcher Anforderungen die Funktion call / 3, die einen Server, eine Anforderung und ein optionales Timeout akzeptiert, das standardmäßig fünf Sekunden beträgt.
Synchrone Anforderungen werden verwendet, wenn wir warten möchten, bis die Antwort tatsächlich vom Server eingeht. Der typische Anwendungsfall ist das Abrufen von Informationen wie z. B. Berechnungsergebnissen, wie im heutigen Beispiel (Denken Sie daran, dass grab_result / 0
Funktion aus einem der vorherigen Abschnitte).
Um synchrone Anforderungen zu verarbeiten, a handle_call / 3
Rückruf wird verwendet. Er akzeptiert eine Anfrage, ein Tupel, das die PID des Servers enthält, und einen Begriff, der den Anruf identifiziert, sowie den aktuellen Status. Im einfachsten Fall sollte es mit einem Tupel antworten : antworten, antworten, neuer zustand
.
Diesen Rückruf jetzt kodieren:
def handle_call (: result, _, state) do : reply, state, state ende
Wie Sie sehen, nichts komplexes. Das Antworten
und der neue Status ist gleich dem aktuellen Status, da ich nach der Rückgabe des Ergebnisses nichts ändern möchte.
Nun die Schnittstelle Ergebnis / 1
Funktion:
def result (pid) GenServer.call (pid,: result) beenden
Das ist es! Die endgültige Verwendung des CalcServers wird nachfolgend beschrieben:
: ok, pid = GenServer.start (CalcServer, 5.1) CalcServer.sqrt (pid) CalcServer.multiply (pid, 2) CalcServer.result (pid) |> IO.puts # => 4.516635916254486
Es wird etwas mühsam, beim Aufruf der Schnittstellenfunktionen immer eine Prozess-ID anzugeben. Glücklicherweise ist es möglich, Ihrem Prozess einen Namen oder eine Bezeichnung zu geben alias. Dies erfolgt beim Start des Servers durch Setzen Name
:
GenServer.start (CalcServer, 5.1, Name:: calc) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts
Beachten Sie, dass ich jetzt keine pid-Datei speichere. Sie sollten jedoch Mustervergleiche durchführen, um sicherzustellen, dass der Server tatsächlich gestartet wurde.
Nun werden die Schnittstellenfunktionen etwas einfacher:
def sqrt do GenServer.cast (: calc,: sqrt) end def multiplizieren (Multiplikator) do GenServer.cast (: calc, : multiply, multiplier)) end def Ergebnis do GenServer.call (: calc,: result) end
Vergessen Sie nicht, dass Sie nicht zwei Server mit demselben Alias starten können.
Alternativ können Sie noch eine andere Schnittstellenfunktion einführen Start / 1
in Ihrem Modul und nutzen Sie das Makro __MODULE __ / 0, das den Namen des aktuellen Moduls als Atom zurückgibt:
defmodule CalcServer verwendet GenServer def start (initial_value). : multiply, multiplier) end def result do GenServer.call (__ MODULE__,: result) end #… end CalcServer.start (6.1) CalcServer.sqrt CalcServer.multiply (2) CalcServer.result |> IO.puts
Ein weiterer Rückruf, der in Ihrem Modul neu definiert werden kann, heißt terminate / 2. Es akzeptiert einen Grund und den aktuellen Status und wird aufgerufen, wenn ein Server gerade beendet wird. Dies kann passieren, wenn Sie beispielsweise ein falsches Argument an den übergeben multiplizieren / 1
Schnittstellenfunktion:
#… CalcServer.multiply (2)
Der Rückruf kann ungefähr so aussehen:
def terminate (_reason, _state) do IO.puts "Der Server wurde beendet" beendet
In diesem Artikel haben wir die Grundlagen der Parallelität in Elixir behandelt und Funktionen und Makros behandelt laichen
, erhalten
, und senden
. Sie haben gelernt, was Prozesse sind, wie sie erstellt werden und wie Sie Nachrichten senden und empfangen. Außerdem haben wir gesehen, wie ein einfacher, langlebiger Serverprozess erstellt wird, der sowohl auf synchrone als auch auf asynchrone Nachrichten reagiert.
Darüber hinaus haben wir das Verhalten von GenServer diskutiert und festgestellt, wie der Code durch die Einführung verschiedener Rückrufe vereinfacht wird. Wir haben mit dem gearbeitet drin
, kündigen
, handle_call
und handle_cast
Callbacks und erstellte einen einfachen Rechenserver. Wenn Ihnen etwas unklar erscheint, zögern Sie nicht, Ihre Fragen zu stellen!
GenServer bietet mehr als alles andere und natürlich ist es unmöglich, alles in einem Artikel zu behandeln. In meinem nächsten Post werde ich was erklären Vorgesetzte sind und wie Sie sie verwenden können, um Ihre Prozesse zu überwachen und nach Fehlern zu beheben. Bis dahin viel Spaß beim Codieren!