
Was ist Asynchronität? Kurz gesagt bedeutet Asynchronität, dass mehrere Aufgaben über einen bestimmten Zeitraum ausgeführt werden. PHP wird in einem einzigen Thread ausgeführt, was bedeutet, dass jeweils nur ein Teil des PHP-Codes ausgeführt werden kann. Dies mag wie eine Einschränkung erscheinen, gibt uns aber tatsächlich mehr Freiheit. Infolgedessen müssen wir uns nicht der Komplexität der Multithread-Programmierung stellen. Andererseits gibt es eine Reihe von Problemen. Wir müssen uns mit Asynchronität befassen. Wir müssen es irgendwie schaffen und koordinieren.
Einführung in die Übersetzung eines Artikels aus dem Blog des Skyeng-Backend-Entwicklers Sergey Zhuk.
Wenn wir beispielsweise zwei parallele HTTP-Anforderungen ausführen, sagen wir, dass sie "parallel ausgeführt" werden. Dies ist normalerweise einfach und unkompliziert. Es treten jedoch Probleme auf, wenn wir die Antworten dieser Anforderungen organisieren müssen, z. B. wenn für eine Anforderung Daten erforderlich sind, die von einer anderen Anforderung empfangen wurden. Daher liegt die größte Schwierigkeit im Asynchronitätsmanagement. Es gibt verschiedene Möglichkeiten, um dieses Problem zu lösen.
PHP bietet derzeit keine native Unterstützung für Abstraktionen auf hoher Ebene zur Steuerung der Asynchronität, und wir müssen Bibliotheken von Drittanbietern wie ReactPHP und Amp verwenden. In den Beispielen in diesem Artikel verwende ich ReactPHP.
Versprechen
Um die Idee von Versprechungen besser zu verstehen, wird ein Beispiel aus der Praxis nützlich sein. Stellen Sie sich vor, Sie sind bei McDonald's und möchten eine Bestellung aufgeben. Sie zahlen Geld dafür und beginnen damit die Transaktion. Als Antwort auf diese Transaktion erwarten Sie einen Hamburger und Pommes. Die Kassiererin gibt das Essen jedoch nicht sofort zurück. Stattdessen erhalten Sie einen Scheck mit der Bestellnummer. Betrachten Sie diesen Scheck als Versprechen für eine zukünftige Bestellung. Jetzt können Sie diesen Check machen und über Ihr köstliches Mittagessen nachdenken. Der erwartete Hamburger und die Pommes sind noch nicht fertig, also stehen Sie und warten, bis Ihre Bestellung abgeschlossen ist. Sobald seine Nummer auf dem Bildschirm erscheint, tauschen Sie den Scheck gegen Ihre Bestellung ein. Das sind die Versprechen:
Ersatz für zukünftigen Wert.
Ein Versprechen ist eine Darstellung für die zukünftige Bedeutung, ein zeitunabhängiger Wrapper, den wir um die Bedeutung wickeln. Es ist uns egal, ob der Wert bereits vorhanden ist oder noch nicht. Wir denken weiterhin genauso über ihn. Stellen Sie sich vor, wir haben drei asynchrone HTTP-Anforderungen, die "parallel" ausgeführt werden, sodass sie zu einem bestimmten Zeitpunkt abgeschlossen werden. Aber wir wollen ihre Antworten irgendwie koordinieren und organisieren. Zum Beispiel möchten wir diese Antworten drucken, sobald sie empfangen wurden, aber mit einer kleinen Einschränkung: Drucken Sie die zweite Antwort erst, wenn die erste empfangen wurde. Hier meine ich, wenn $ Versprechen1 erfüllt ist, dann drucken wir es. Wenn jedoch $ Versprechen2 zuerst erfüllt wird, drucken wir es nicht aus, da $ Versprechen1 noch in Bearbeitung ist. Stellen Sie sich vor, wir versuchen, drei wettbewerbsfähige Anfragen so anzupassen, dass sie für den Endbenutzer wie eine schnelle Anfrage aussehen.
Wie können wir dieses Problem mit Versprechungen lösen? Zunächst brauchen wir eine Funktion, die ein Versprechen zurückgibt. Wir können drei solcher Versprechen sammeln und sie dann zusammenstellen. Hier ist ein gefälschter Code dafür:
<?php use React\Promise\Promise; function fakeResponse(string $url, callable $callback) { $callback("response for $url"); } function makeRequest(string $url) { return new Promise(function(callable $resolve) use ($url) { fakeResponse($url, $resolve); }); }
Hier habe ich zwei Funktionen:
fakeResponse (Zeichenfolge $ url, aufrufbarer $ Rückruf) enthält eine fest codierte Antwort und ermöglicht den angegebenen Rückruf mit dieser Antwort.
makeRequest (string $ url) gibt ein Versprechen zurück, das fakeResponse () verwendet, um anzuzeigen, dass die Anforderung abgeschlossen wurde.
Aus dem Client-Code rufen wir einfach die Funktion makeRequest () auf und erhalten die Versprechen:
<?php $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3');
Es war einfach, aber jetzt müssen wir diese Antworten irgendwie sortieren. Wir möchten erneut, dass die Antwort aus dem zweiten Versprechen erst nach Abschluss des ersten Versprechens gedruckt wird. Um dieses Problem zu lösen, können Sie eine Reihe von Versprechungen erstellen:
<?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; });
Im obigen Code beginnen wir mit $ versprechen1 . Sobald es fertig ist, drucken wir seinen Wert. Es ist uns egal, wie lange es dauert: weniger als eine Sekunde oder eine Stunde. Sobald das Versprechen erfüllt ist, werden wir seinen Wert drucken. Und dann warten wir auf $ versprechen2 . Und hier können wir zwei Szenarien haben:
$ versprechen2 ist bereits abgeschlossen und wir drucken sofort seinen Wert;
$ versprechen2 wird noch erfüllt und wir warten.
Dank der Verkettung von Versprechungen müssen wir uns keine Sorgen mehr machen, ob ein Versprechen erfüllt wurde oder nicht. Promis ist nicht zeitabhängig und verbirgt dadurch seine Zustände vor uns (dabei bereits abgeschlossen oder storniert).
So können Sie die Asynchronität mit Versprechungen steuern. Und es sieht gut aus, die Kette der Versprechen ist viel hübscher und verständlicher als eine Reihe verschachtelter Rückrufe.
Generatoren
In PHP sind Generatoren eine integrierte Sprachunterstützung für Funktionen, die angehalten und dann fortgesetzt werden können. Wenn die Codeausführung in einem solchen Generator stoppt, sieht es aus wie ein kleines blockiertes Programm. Aber außerhalb dieses Programms, außerhalb des Generators, funktioniert alles andere weiter. Das ist die ganze Magie und Kraft der Generatoren.
Wir können den Generator buchstäblich vor Ort anhalten, um auf die Erfüllung des Versprechens zu warten. Die Grundidee ist, Versprechen und Generatoren zusammen zu verwenden. Sie übernehmen die Kontrolle über die Asynchronität, und wir rufen Yield nur auf, wenn wir den Generator anhalten müssen. Hier ist das gleiche Programm, aber jetzt verbinden wir Generatoren und Versprechen:
<?php use Recoil\React\ReactKernel;
Für diesen Code verwende ich die Bibliothek recoilphp / recoil , mit der Sie ReactKernel :: start () aufrufen können . Mit Recoil können PHP-Generatoren verwendet werden, um asynchrone ReactPHP-Versprechen auszuführen.
Hier führen wir immer noch drei Abfragen parallel durch, aber jetzt sortieren wir die Antworten mit dem Schlüsselwort yield . Und wieder zeigen wir die Ergebnisse am Ende jedes Versprechens an, aber erst nach dem vorherigen.
Coroutinen
Coroutinen sind eine Möglichkeit, eine Operation oder einen Prozess in Blöcke zu unterteilen, wobei in jedem dieser Blöcke eine gewisse Ausführung erfolgt. Infolgedessen stellt sich heraus, dass der gesamte Vorgang nicht gleichzeitig ausgeführt wird (was zu einem spürbaren Einfrieren der Anwendung führen kann), sondern schrittweise ausgeführt wird, bis alle erforderlichen Arbeiten abgeschlossen sind.
Jetzt, da wir unterbrechbare und erneuerbare Generatoren haben, können wir sie verwenden, um asynchronen Code mit Versprechungen in einer bekannteren synchronen Form zu schreiben. Mit PHP-Generatoren und Versprechungen können Sie Rückrufe vollständig beseitigen. Die Idee ist, dass, wenn wir ein Versprechen abgeben (unter Verwendung des Yield Call), eine Coroutine es abonniert. Corutin macht eine Pause und wartet, bis das Versprechen erfüllt ist (abgeschlossen oder storniert). Sobald das Versprechen erfüllt ist, wird Coroutine weiterhin erfüllt. Nach erfolgreichem Abschluss sendet das Coroutine-Versprechen den empfangenen Wert mithilfe des Aufrufs Generator :: send ($ value) an den Generatorkontext zurück. Wenn das Versprechen fehlschlägt, löst Corutin mit dem Aufruf Generator :: throw () eine Ausnahme durch den Generator aus. Ohne Rückrufe können wir asynchronen Code schreiben, der fast wie der übliche synchrone Code aussieht.
Sequentielle Ausführung
Bei Verwendung von Coroutine ist jetzt die Ausführungsreihenfolge im asynchronen Code von Bedeutung. Der Code wird genau an der Stelle ausgeführt, an der das Yield-Schlüsselwort aufgerufen wird, und dann angehalten, bis das Versprechen erfüllt ist. Betrachten Sie den folgenden Code:
<?php use Recoil\React\ReactKernel;
Versprechen1: wird hier angezeigt, dann wird die Ausführung angehalten und gewartet. Sobald das Versprechen von makeRequest ('url1') erfüllt ist, drucken wir das Ergebnis aus und fahren mit der nächsten Codezeile fort.
Fehlerbehandlung
Der Promises / A + Promise-Standard besagt, dass jedes Promise die Methoden then () und catch () enthält . Über diese Schnittstelle können Sie Ketten aus Versprechungen erstellen und optional Fehler abfangen. Betrachten Sie den folgenden Code:
<?php operation()->then(function ($result) { return anotherOperation($result); })->then(function ($result) { return yetAnotherOperation($result); })->then(function ($result) { echo $result; });
Hier haben wir eine Kette von Versprechungen, die das Ergebnis jedes vorherigen Versprechens an das nächste weitergibt. Es gibt jedoch keinen catch () -Block in dieser Kette, hier gibt es keine Fehlerbehandlung. Wenn ein Versprechen in einer Kette fehlschlägt, wird die Codeausführung zum nächsten Fehlerbehandler in der Kette verschoben. In unserem Fall bedeutet dies, dass das ausstehende Versprechen ignoriert wird und alle Fehler, die weggeworfen werden, für immer verschwinden. Bei Coroutinen tritt die Fehlerbehandlung in den Vordergrund. Wenn eine asynchrone Operation fehlschlägt, wird eine Ausnahme ausgelöst:
<?php use Recoil\React\ReactKernel; use React\Promise\RejectedPromise;
Asynchronen Code lesbar machen
Generatoren haben einen wirklich wichtigen Nebeneffekt, mit dem wir die Asynchronität steuern können und der das Problem der Lesbarkeit von asynchronem Code löst. Es ist schwer zu verstehen, wie der asynchrone Code ausgeführt wird, da der Ausführungsthread ständig zwischen verschiedenen Teilen des Programms wechselt. Unser Gehirn arbeitet jedoch grundsätzlich synchron und mit einem Thread. Zum Beispiel planen wir unseren Tag sehr konsequent: einen, dann einen anderen und so weiter. Aber asynchroner Code funktioniert nicht so, wie unser Gehirn es gewohnt ist zu denken. Selbst eine einfache Kette von Versprechungen sieht möglicherweise nicht gut lesbar aus:
<?php $promise1 ->then('var_dump') ->then(function() use ($promise2) { return $promise2; }) ->then('var_dump') ->then(function () use ($promise3) { return $promise3; }) ->then('var_dump') ->then(function () { echo 'Complete'; });
Wir müssen es mental zerlegen, um zu verstehen, was dort passiert. Wir brauchen also ein anderes Muster, um die Asynchronität zu steuern. Kurz gesagt, Generatoren bieten eine Möglichkeit, asynchronen Code so zu schreiben, dass er synchron aussieht.
Versprechen und Generatoren kombinieren das Beste aus beiden Welten: Wir erhalten asynchronen Code mit großer Leistung, aber gleichzeitig sieht er synchron, linear und sequentiell aus. Mit Coroutinen können Sie die Asynchronität ausblenden, die bereits zu einem Implementierungsdetail wird. Gleichzeitig sieht unser Code so aus, als wäre unser Gehirn an das Denken gewöhnt - linear und sequentiell.
Wenn wir über ReactPHP sprechen, können wir die RecoilPHP-Bibliothek verwenden, um Versprechen in Form von Coroutine zu schreiben. In Amp sind Coroutinen sofort verfügbar.