
Qu'est-ce que l'asynchronie? En bref, l'asynchronie signifie effectuer plusieurs tâches sur une période de temps spécifique. PHP s'exécute dans un seul thread, ce qui signifie qu'un seul morceau de code PHP peut être exécuté à un moment donné. Cela peut sembler une limitation, mais cela nous donne en fait plus de liberté. En conséquence, nous n'avons pas à faire face à toute la complexité associée à la programmation multithread. Mais d'un autre côté, il y a un ensemble de problèmes. Nous devons faire face à l'asynchronie. Nous devons en quelque sorte le gérer et le coordonner.
Présentation de la traduction d'un article du blog du développeur backend Skyeng Sergey Zhuk.
Par exemple, lorsque nous exécutons deux requêtes HTTP parallèles, nous disons qu'elles "s'exécutent en parallèle". C'est généralement facile et simple à faire, mais des problèmes surviennent lorsque nous devons rationaliser les réponses de ces demandes, par exemple, lorsqu'une demande nécessite des données reçues d'une autre demande. C'est donc dans la gestion asynchrone que réside la plus grande difficulté. Il existe plusieurs façons de résoudre ce problème.
Il n'y a actuellement pas de support intégré pour les abstractions de haut niveau pour la gestion de l'asynchronie en PHP, et nous devons utiliser des bibliothèques tierces telles que ReactPHP et Amp. Dans les exemples de cet article, j'utilise ReactPHP.
Promesses
Pour mieux comprendre l'idée des promesses, un exemple concret vous sera utile. Imaginez que vous êtes chez McDonald's et que vous souhaitez passer une commande. Vous payez de l'argent et commencez ainsi la transaction. En réponse à cette transaction, vous vous attendez à obtenir un hamburger et des frites. Mais la caissière ne rend pas immédiatement la nourriture. Au lieu de cela, vous recevez un chèque avec le numéro de commande. Considérez ce chèque comme une promesse pour une future commande. Vous pouvez maintenant prendre ce chèque et commencer à penser à votre délicieux déjeuner. Le hamburger et les frites attendus ne sont pas encore prêts, vous devez donc vous lever et attendre que votre commande soit terminée. Dès que son numéro apparaît à l'écran, vous échangez le chèque contre votre commande. Ce sont les promesses:
Remplace la valeur future.
Une promesse est une représentation d'une signification future, une enveloppe indépendante du temps que nous enroulons autour d'une signification. Peu nous importe que la valeur soit déjà là ou pas encore. Nous continuons de penser à lui de la même manière. Imaginez que nous ayons trois requêtes HTTP asynchrones qui sont exécutées «en parallèle», de sorte qu'elles seront terminées à environ un moment donné. Mais nous voulons en quelque sorte coordonner et organiser leurs réponses. Par exemple, nous voulons imprimer ces réponses dès qu'elles sont reçues, mais avec une légère restriction: n'imprimez pas la deuxième réponse avant d'avoir reçu la première. Ici, je veux dire que si $ promesse1 est remplie, nous l'imprimons. Mais si $ promesse2 est remplie en premier, nous ne l'imprimons pas, car $ promesse1 est toujours en cours. Imaginez que nous essayons d'adapter trois demandes concurrentes de telle sorte que pour l'utilisateur final, elles ressemblent à une demande rapide.
Alors, comment pouvons-nous résoudre ce problème avec des promesses? Tout d'abord, nous avons besoin d'une fonction qui renvoie une promesse. Nous pouvons recueillir trois de ces promesses, puis les rassembler. Voici un faux code pour cela:
<?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); }); }
Ici, j'ai deux fonctions:
fakeResponse (chaîne $ url, callable $ callback) contient une réponse codée en dur et autorise le rappel spécifié avec cette réponse;
makeRequest (string $ url) renvoie une promesse qui utilise fakeResponse () pour indiquer que la demande est terminée.
A partir du code client, nous appelons simplement la fonction makeRequest () et obtenons les promesses:
<?php $promise1 = makeRequest('url1'); $promise2 = makeRequest('url2'); $promise3 = makeRequest('url3');
C'était simple, mais maintenant nous devons trier ces réponses d'une manière ou d'une autre. Encore une fois, nous voulons que la réponse de la deuxième promesse ne soit imprimée qu'après l'achèvement de la première. Pour résoudre ce problème, vous pouvez construire une chaîne de promesses:
<?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'; });
Dans le code ci-dessus, nous commençons par $ promise1 . Une fois terminé, nous imprimons sa valeur. Peu nous importe le temps que cela prend: moins d’une seconde ou d’une heure. Dès que la promesse sera terminée, nous imprimerons sa valeur. Et puis nous attendons $ promesse2 . Et ici, nous pouvons avoir deux scénarios:
$ promise2 est déjà terminé et nous imprimons immédiatement sa valeur;
$ promise2 est toujours en train de se réaliser et nous attendons.
Grâce à l'enchaînement des promesses, nous n'avons plus à nous soucier de la réalisation ou non d'une promesse. Promis ne dépend pas du temps, et de ce fait il nous cache ses états (en cours, déjà terminés ou annulés).
C'est ainsi que vous pouvez contrôler l'asynchronie avec les promesses. Et cela a l'air génial, la chaîne de promesses est beaucoup plus jolie et plus compréhensible qu'un tas de rappels imbriqués.
Générateurs
En PHP, les générateurs sont un support de langage intégré pour les fonctions qui peuvent être suspendues puis poursuivies. Lorsque l'exécution de code à l'intérieur d'un tel générateur s'arrête, cela ressemble à un petit programme bloqué. Mais en dehors de ce programme, en dehors du générateur, tout le reste continue de fonctionner. C'est toute la magie et la puissance des générateurs.
Nous pouvons littéralement suspendre le générateur localement pour attendre la fin de la promesse. L'idée de base est d'utiliser ensemble les promesses et les générateurs. Ils prennent le contrôle de l'asynchronie, et nous appelons simplement yield lorsque nous devons suspendre le générateur. Voici le même programme, mais maintenant nous connectons des générateurs et des promesses:
<?php use Recoil\React\ReactKernel;
Pour ce code, j'utilise la bibliothèque recoilphp / recoil , qui vous permet d'appeler ReactKernel :: start () . Recoil permet d'utiliser des générateurs PHP pour exécuter les promesses asynchrones ReactPHP.
Ici, nous effectuons toujours trois requêtes en parallèle, mais maintenant nous trions les réponses à l'aide du mot clé yield . Et encore une fois, nous affichons les résultats à la fin de chaque promesse, mais seulement après la précédente.
Coroutines
Les coroutines sont un moyen de diviser une opération ou un processus en morceaux, avec une certaine exécution à l'intérieur de chacun de ces morceaux. En conséquence, il s'avère qu'au lieu d'effectuer l'opération entière à la fois (ce qui peut entraîner un gel notable de l'application), elle sera effectuée progressivement jusqu'à ce que toute la quantité de travail nécessaire soit terminée.
Maintenant que nous avons des générateurs interruptibles et renouvelables, nous pouvons les utiliser pour écrire du code asynchrone avec des promesses sous une forme synchrone plus familière. En utilisant des générateurs PHP et des promesses, vous pouvez complètement vous débarrasser des rappels. L'idée est que lorsque nous donnons une promesse (en utilisant l'appel de rendement), une coroutine y souscrit. Corutin marque une pause et attend que la promesse soit terminée (terminée ou annulée). Dès que la promesse sera terminée, coroutine continuera de se réaliser. Une fois terminée, la promesse coroutine renvoie la valeur reçue au contexte du générateur à l'aide de l'appel Generator :: send ($ value) . Si la promesse échoue, Corutin lève une exception via le générateur à l'aide de l'appel Generator :: throw () . En l'absence de rappels, nous pouvons écrire du code asynchrone qui ressemble presque à celui synchrone habituel.
Exécution séquentielle
Lors de l'utilisation de coroutine, l'ordre d'exécution en code asynchrone est désormais important. Le code est exécuté exactement à l'endroit où le mot clé yield est appelé, puis mis en pause jusqu'à ce que la promesse soit terminée. Considérez le code suivant:
<?php use Recoil\React\ReactKernel;
Promise1: s'affiche ici , puis l'exécution s'interrompt et attend. Dès que la promesse de makeRequest ('url1') est terminée, nous imprimons son résultat et passons à la ligne de code suivante.
Gestion des erreurs
La norme Promises / A + Promise indique que chaque promesse contient les méthodes then () et catch () . Cette interface vous permet de créer des chaînes à partir de promesses et éventuellement de détecter des erreurs. Considérez le code suivant:
<?php operation()->then(function ($result) { return anotherOperation($result); })->then(function ($result) { return yetAnotherOperation($result); })->then(function ($result) { echo $result; });
Ici, nous avons une chaîne de promesses qui transmet le résultat de chaque promesse précédente à la suivante. Mais il n'y a pas de bloc catch () dans cette chaîne, il n'y a pas de gestion d'erreur ici. Lorsqu'une promesse dans une chaîne échoue, l'exécution de code se déplace vers le gestionnaire d'erreurs le plus proche de la chaîne. Dans notre cas, cela signifie que la promesse en suspens sera ignorée et que toute erreur rejetée disparaîtra à jamais. Avec les coroutines, la gestion des erreurs apparaît au premier plan. Si une opération asynchrone échoue, une exception sera levée:
<?php use Recoil\React\ReactKernel; use React\Promise\RejectedPromise;
Rendre le code asynchrone lisible
Les générateurs ont un effet secondaire vraiment important que nous pouvons utiliser pour contrôler l'asynchronie et qui résout le problème de lisibilité du code asynchrone. Il est difficile pour nous de comprendre comment le code asynchrone sera exécuté car le thread d'exécution bascule constamment entre les différentes parties du programme. Cependant, notre cerveau fonctionne essentiellement de manière synchrone et monothread. Par exemple, nous planifions notre journée de manière très cohérente: en faire une, puis une autre, etc. Mais le code asynchrone ne fonctionne pas de la façon dont nos cerveaux sont habitués à penser. Même une simple chaîne de promesses peut ne pas sembler très lisible:
<?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'; });
Nous devons le démonter mentalement afin de comprendre ce qui se passe là-bas. Nous avons donc besoin d'un modèle différent pour contrôler l'asynchronie. Et en bref, les générateurs fournissent un moyen d'écrire du code asynchrone pour qu'il ressemble à synchrone.
Les promesses et les générateurs combinent le meilleur des deux mondes: nous obtenons du code asynchrone avec de grandes performances, mais en même temps, il ressemble à synchrone, linéaire et séquentiel. Les coroutines vous permettent de masquer l'asynchronie, qui devient déjà un détail d'implémentation. Et notre code ressemble en même temps à notre cerveau est habitué à penser - linéairement et séquentiellement.
Si nous parlons de ReactPHP , alors nous pouvons utiliser la bibliothèque RecoilPHP pour écrire des promesses sous forme de coroutine. Dans Amp, les coroutines sont disponibles dès la sortie de la boîte.