Tracks
/
Elixir
Elixir
/
Übungen
/
Inspektion eines RPN-Rechners
Inspektion eines RPN-Rechners

Inspektion eines RPN-Rechners

Lernübung

Einführung

Elixir-Prozesse sind isoliert und teilen standardmäßig nichts. Wenn ein nicht verlinkter Kindprozess abstürzt, ist sein Elternprozess nicht betroffen.

Dieses Verhalten lässt sich ändern, indem man Prozesse miteinander verlinkt. Wenn zwei Prozesse verlinkt sind, wird ein Fehler in einem Prozess auf den anderen Prozess übertragen. Links sind bidirektional.

Prozesse können mit spawn_link/1 bereits verlinkt zum aufrufenden Prozess gestartet werden, was eine atomare Operation ist, oder sie können später mit Process.link/1 verlinkt werden.

Das Verlinken von Prozessen kann bei parallelisierter Arbeit nützlich sein, wenn ein Arbeitspaket nicht fortgesetzt werden soll, falls ein anderes Arbeitspaket nicht fertig wird.

Exits abfangen

Links können auch zum Überwachen von Prozessen verwendet werden. Wenn ein Prozess Exits abfängt, stürzt er nicht ab, wenn ein mit ihm verlinkter Prozess abstürzt. Stattdessen empfängt er eine Nachricht über den Absturz. Dadurch kann er den Absturz elegant behandeln, zum Beispiel indem er den abgestürzten Prozess neu startet.

Ein Prozess kann so konfiguriert werden, dass er Exits abfängt, indem er Process.flag(:trap_exit, true) aufruft. Beachte, dass Process.flag/2 den alten Wert des Flags zurückgibt, nicht den neuen.

Die Nachricht, die an den Prozess gesendet wird, falls ein verlinkter Prozess abstürzt, entspricht dem Muster {:EXIT, from, reason}, wobei from eine PID ist. Wenn reason etwas anderes als das Atom :normal ist, bedeutet das, dass der Prozess abgestürzt oder gewaltsam beendet wurde.

Tasks

Tasks sind Prozesse, die eine bestimmte Operation ausführen sollen. Normalerweise kommunizieren sie nicht mit anderen Prozessen, aber sie können ein Ergebnis an den Prozess zurückgeben, der sie gestartet hat.

Tasks werden häufig verwendet, um Arbeit zu parallelisieren.

async/await

Um einen Task zu starten, verwende Task.async/1. Die Funktion nimmt eine anonyme Funktion als Argument und führt sie in einem neuen Prozess aus, der mit dem aufrufenden Prozess verlinkt ist. Sie gibt eine %Task{}-Struct zurück.

Um das Ergebnis der Ausführung zu erhalten, übergibst du die %Task{}-Struct an Task.await/2. Sie wartet, bis der Task fertig ist, und gibt sein Ergebnis zurück. Das zweite Argument ist ein Timeout in Millisekunden mit dem Standardwert 5000.

Beachte, dass der Prozess, der den Task gestartet hat, zwischen dem Starten und dem Warten auf den Task nicht blockiert ist und andere Operationen ausführen kann.

Jeder mit Task.async/1 gestartete Task sollte abgewartet werden, da er eine Nachricht an den aufrufenden Prozess sendet. Task.await/2 kann für jeden Task nur einmal aufgerufen werden.

Wenn du einen Task nur wegen seiner Seiteneffekte starten möchtest, verwende Task.start/1 oder Task.start_link/1. Task.start/1 startet einen Task, der nicht mit dem aufrufenden Prozess verlinkt ist, und Task.start_link/1 startet einen Task, der mit dem aufrufenden Prozess verlinkt ist. Beide Funktionen geben ein {:ok, pid}-Tupel zurück.

Anleitung

Deine Arbeit bei Instruments of Texas an einem experimentellen UPN-Rechner geht weiter. Dein Team hat ein paar Prototypen gebaut, die gründlich geprüft werden müssen, um den besten auszuwählen, der in Massenproduktion gehen kann.

Du möchtest zwei Arten von Prüfungen durchführen.

Erstens eine Zuverlässigkeitsprüfung, die Eingaben erkennt, bei denen der geprüfte Rechner entweder abstürzt oder nicht schnell genug antwortet. Um Fehler zu isolieren, müssen die Berechnungen für jede Eingabe in einem eigenen Prozess ausgeführt werden. Mit Links und dem Abfangen von Exits im aufrufenden Prozess kannst du feststellen, ob die Berechnung beendet oder abgestürzt ist.

Zweitens eine Korrektheitsprüfung, die prüft, ob das vom Rechner für eine gegebene Eingabe zurückgegebene Ergebnis den Erwartungen entspricht. Nur Rechner, die die Zuverlässigkeitsprüfung bereits bestanden haben, werden einer Korrektheitsprüfung unterzogen, Abstürze sind also kein Problem. Die Operationen sollten jedoch parallel ausgeführt werden, um den Vorgang zu beschleunigen. Damit sind sie der perfekte Anwendungsfall für asynchrone Tasks.

1. Starte eine Zuverlässigkeitsprüfung für eine einzelne Eingabe

Implementiere die Funktion RPNCalculatorInspection.start_reliability_check/2. Sie sollte zwei Argumente entgegennehmen: eine Funktion (den Rechner) und eine Eingabe für den Rechner. Sie sollte eine Map zurückgeben, die die Eingabe und die PID des gestarteten Prozesses enthält.

Der gestartete Prozess sollte die angegebene Rechnerfunktion mit der angegebenen Eingabe aufrufen. Der Prozess sollte mit dem aufrufenden Prozess verlinkt sein.

RPNCalculatorInspection.start_reliability_check(fn _ -> 0 end, "2 3 +")
# => %{input: "2 3 +", pid: #PID<0.169.0>}

2. Interpretiere die Ergebnisse einer Zuverlässigkeitsprüfung

Implementiere die Funktion RPNCalculatorInspection.await_reliability_check_result/2. Sie sollte zwei Argumente entgegennehmen. Das erste Argument ist eine Map mit der Eingabe der Zuverlässigkeitsprüfung und der PID des Prozesses, der die Zuverlässigkeitsprüfung für diese Eingabe ausführt, wie sie von RPNCalculatorInspection.start_reliability_check/2 zurückgegeben wird. Das zweite Argument ist eine Map, die als Akkumulator für die Ergebnisse von Zuverlässigkeitsprüfungen mit verschiedenen Eingaben dient.

Die Funktion sollte auf eine Exit-Nachricht warten.

Wenn sie von demselben Prozess, der die Zuverlässigkeitsprüfung ausführt, eine Exit-Nachricht ({:EXIT, from, reason}) mit dem Grund :normal erhält, sollte sie die Ergebnis-Map mit dem Wert :ok unter dem Schlüssel input zurückgeben.

Wenn sie von demselben Prozess, der die Zuverlässigkeitsprüfung ausführt, eine Exit-Nachricht mit einem anderen Grund erhält, sollte sie die Ergebnis-Map mit dem Wert :error unter dem Schlüssel input zurückgeben.

Wenn sie innerhalb von 100 ms keine Nachricht erhält, die diesen Kriterien entspricht, sollte sie die Ergebnis-Map mit dem Wert :timeout unter dem Schlüssel input zurückgeben.

# when an exit message is waiting for the process in its inbox
send(self(), {:EXIT, pid, :normal})

RPNCalculatorInspection.await_reliability_check_result(
  %{input: "5 7 -", pid: pid},
  %{}
)

# => %{"5 7 -" => :ok}

# when there are no messages in the process inbox
RPNCalculatorInspection.await_reliability_check_result(
  %{input: "3 2 *", pid: pid},
  %{"5 7 -" => :ok}
)

# => %{"5 7 -" => :ok, "3 2 *" => :timeout}

3. Führe eine parallele Zuverlässigkeitsprüfung für viele Eingaben durch

Implementiere die Funktion RPNCalculatorInspection.reliability_check/2. Sie sollte zwei Argumente entgegennehmen: eine Funktion (den Rechner) und eine Liste von Eingaben für den Rechner.

Für jede Eingabe in der Liste sollte sie die Zuverlässigkeitsprüfung mit start_reliability_check/2 in einem neuen verlinkten Prozess starten. Danach sollte sie für jeden so gestarteten Prozess mit await_reliability_check_result/2 auf dessen Ergebnisse warten.

Bevor sie irgendwelche Prozesse startet, muss die Funktion den aktuellen Prozess so markieren, dass er Exits abfängt, damit er Exit-Nachrichten empfangen kann. Danach sollte sie dieses Flag wieder auf seinen ursprünglichen Wert setzen.

Die Funktion sollte eine Map mit den Ergebnissen der Zuverlässigkeitsprüfungen aller Eingaben zurückgeben.

fake_broken_calculator = fn input ->
  if String.ends_with?(input, "*"), do: raise("oops")
end

inputs = ["2 3 +", "10 3 *", "20 2 /"]

RPNCalculatorInspection.reliability_check(fake_broken_calculator, inputs)
# => %{
#       "2 3 +" => :ok,
#       "10 3 *" => :error,
#       "20 2 /" => :ok
#     }

4. Führe eine parallele Korrektheitsprüfung für viele Eingaben durch

Implementiere die Funktion RPNCalculatorInspection.correctness_check/2. Sie sollte zwei Argumente entgegennehmen: eine Funktion (den Rechner) und eine Liste von Eingaben für den Rechner.

Für jede Eingabe in der Liste sollte sie einen asynchronen Task starten, der den Rechner mit der angegebenen Eingabe aufruft. Danach sollte sie für jeden so gestarteten Task 100 ms lang auf dessen Ergebnisse warten.

fast_cheating_calculator = fn input -> 14 end
inputs = ["13 1 +", "50 2 *", "1000 2 /"]
RPNCalculatorInspection.correctness_check(fast_cheating_calculator, inputs)
# => [14, 14, 14]
Über GitHub bearbeiten Der Link öffnet sich in einem neuen Fenster oder Tab
Elixir Exercism

Bereit, mit Inspektion eines RPN-Rechners zu starten?

Melde dich bei Exercism an, um Elixir mit 58 Konzepte168 Übungen und echtem menschlichen Mentoring zu lernen und zu meistern, alles kostenlos.