PHP asynchrone

Il y a dix ans, nous avions une pile LAMP classique: Linux, Apache, MySQL et PHP, qui fonctionnait en mode lent de mod_php. Le monde a changé, et avec lui l'importance de la vitesse. PHP-FPM est apparu, ce qui a permis d'augmenter considérablement les performances des solutions en PHP, et de ne pas réécrire de toute urgence vers quelque chose de plus rapide.

En parallĂšle, la bibliothĂšque ReactPHP a Ă©tĂ© dĂ©veloppĂ©e en utilisant le concept Event Loop pour traiter les signaux du systĂšme d'exploitation et prĂ©senter les rĂ©sultats des opĂ©rations asynchrones. Le dĂ©veloppement de l'idĂ©e de ReactPHP - AMPHP. Cette bibliothĂšque utilise la mĂȘme boucle d'Ă©vĂ©nement, mais prend en charge les coroutines, contrairement Ă  ReactPHP. Ils vous permettent d'Ă©crire du code asynchrone qui ressemble Ă  synchrone. C'est peut-ĂȘtre le cadre le plus actuel pour dĂ©velopper des applications asynchrones en PHP.



Mais la vitesse est de plus en plus nĂ©cessaire, les outils ne sont dĂ©jĂ  pas suffisants, donc l'idĂ©e de programmation asynchrone en PHP est l'un des moyens d'accĂ©lĂ©rer le traitement des requĂȘtes et de mieux utiliser les ressources.

C'est ce dont Anton Shabovta va parler (zloyusr) Est développeur chez Onliner. Expérience de plus de 10 ans: j'ai commencé avec des applications bureautiques en C / C ++, puis je suis passé au développement web en PHP. Il écrit des projets "Home" en C # et Python 3, et en PHP il expérimente DDD, CQRS, Event Sourcing, Async Multitasking.

Cet article est basé sur une transcription du rapport d'Anton sur PHP Russie 2019 . Nous y comprendrons les opérations bloquantes et non bloquantes en PHP, nous étudierons de l'intérieur la structure des boucles d'événements et des primitives asynchrones, telles que Promise et coroutines. Enfin, nous découvrirons ce qui nous attend dans ext-async, AMPHP 3 et PHP 8.


Nous introduisons quelques définitions. Pendant longtemps, j'ai essayé de trouver une définition exacte des opérations asynchrones et asynchrones, mais je n'ai pas trouvé et écrit la mienne.
L'asynchronie est la capacité d'un systÚme logiciel à ne pas bloquer le thread principal d'exécution.
Une opération asynchrone est une opération qui ne bloque pas le flux d'exécution d'un programme tant qu'elle n'est pas terminée.

Cela semble simple, mais vous devez d'abord comprendre quelles opérations bloquent le flux d'exécution.

Opérations de blocage


PHP est un langage interpréteur. Il lit le code ligne par ligne, traduit dans ses instructions et exécute. Sur quelle ligne de l'exemple ci-dessous le code sera-t-il bloqué?

public function update(User $user)
{
    try {
        $sql = 'UPDATE users SET ...';
        return $this->connection->execute($sql, $user->data());
    } catch (\PDOException $error) {
        log($error->getMessage());
    }

    return 0;
}

Si nous nous connectons Ă  la base de donnĂ©es via PDO, le thread d'exĂ©cution sera bloquĂ© sur la chaĂźne de requĂȘte SQL serveur: return $this->connection->execute($sql, $user->data());.

C'est parce que PHP ne sait pas combien de temps le serveur SQL traitera cette requĂȘte et s'il s'exĂ©cutera du tout. Il attend une rĂ©ponse du serveur et le programme n'a pas Ă©tĂ© exĂ©cutĂ© pendant tout ce temps.

PHP bloque également le flux d'exécution sur toutes les opérations d'E / S.

  • SystĂšme de fichiers : fwrite, file_get_contents.
  • Bases de donnĂ©es : PDOConnection, RedisClient. Presque toutes les extensions pour connecter une base de donnĂ©es fonctionnent par dĂ©faut en mode blocage.
  • Processus : exec, system, proc_open. Ce sont des opĂ©rations de blocage, car tout travail avec des processus est construit via des appels systĂšme.
  • Travailler avec stdin / stdout : readline, echo, print.

De plus, l'exécution est bloquée sur les temporisateurs : sleep, usleep. Ce sont des opérations dans lesquelles nous disons explicitement au thread de s'endormir pendant un certain temps. PHP sera inactif tout ce temps.

Client SQL asynchrone


Mais le PHP moderne est un langage à usage général, et pas seulement pour le Web comme PHP / FI en 1997. Par conséquent, nous pouvons écrire un client SQL asynchrone à partir de zéro. La tùche n'est pas la plus triviale, mais résoluble.

public function execAsync(string $query, array $params = [])
{
    $socket = stream_socket_client('127.0.0.1:3306', ...);

    stream_set_blocking($socket, false);

    $data = $this->packBinarySQL($query, $params);
    
    socket_write($socket, $data, strlen($data));
}

Que fait un tel client? Il se connecte Ă  notre serveur SQL, met le socket en mode non bloquant, emballe la requĂȘte dans un format binaire que le serveur SQL comprend, Ă©crit des donnĂ©es dans le socket.

Puisque le socket est en mode non bloquant, l'opération d'écriture depuis PHP est rapide.

Mais que reviendra-t-il Ă  la suite d'une telle opĂ©ration? Nous ne savons pas ce que le serveur SQL rĂ©pondra. Le traitement de la demande peut prendre du temps ou pas du tout. Mais quelque chose doit ĂȘtre retournĂ©? Si nous utilisons PDO et appelons la updaterequĂȘte sur le serveur SQL, nous sommes renvoyĂ©s affected rows- le nombre de lignes modifiĂ©es par cette requĂȘte. Nous ne pouvons pas encore le retourner, donc nous promettons seulement un retour.

Promettre


Il s'agit d'un concept issu du monde de la programmation asynchrone.
Promise est un objet wrapper sur le résultat d'une opération asynchrone. De plus, le résultat de l'opération nous est encore inconnu.
Malheureusement, il n'y a pas de norme Promise unique et il n'est pas possible de transférer directement des normes du monde JavaScript vers PHP.

Fonctionnement de Promise


Puisqu'il n'y a pas encore de résultat, nous pouvons seulement en établir callbacks.



Lorsque des données sont disponibles, il est nécessaire d'exécuter un rappel onResolve.



Si une erreur se produit, un rappel sera exécuté onRejectpour gérer l'erreur.



L'interface Promise ressemble Ă  ceci.

interface Promise
{
    const
        STATUS_PENDING = 0,
        STATUS_RESOLVED = 1,
        STATUS_REJECTED = 2
    ;

    public function onResolve(callable $callback);
    public function onReject(callable $callback);
    public function resolve($data);
    public function reject(\Throwable $error);
}

Promise a un statut et des mĂ©thodes pour dĂ©finir des rappels et remplir ( resolve) Promise avec des donnĂ©es ou error ( reject). Mais il y a des diffĂ©rences et des variations. Les mĂ©thodes peuvent ĂȘtre appelĂ©es diffĂ©remment, ou Ă  la place de mĂ©thodes distinctes pour Ă©tablir des rappels, resolveet il rejectpeut y en avoir une, comme dans AMPHP, par exemple.

Souvent des techniques pour remplir Promise resolveet rejectretirer dans un objet sĂ©parĂ© la fonction asynchrone Ă  Ă©tat de stockage diffĂ©rĂ© . Elle peut ĂȘtre considĂ©rĂ©e comme une sorte d'usine pour Promise. C'est unique: un diffĂ©rĂ© fait une promesse.



Comment appliquer cela dans le client SQL si nous dĂ©cidons de l'Ă©crire nous-mĂȘmes?

Client SQL asynchrone


Tout d'abord, nous avons créé Deferred, fait tout le travail avec les sockets, noté les données et renvoyé Promise - tout est simple.

public function execAsync(string $query, array $params = [])
{
    $deferred = new Deferred;

    $socket = stream_socket_client('127.0.0.1:3306', ...);
    stream_set_blocking($socket, false);

    $data = $this->packBinarySQL($query, $params);
    socket_write($socket, $data, strlen($data));

    return $deferred->promise();
}

Lorsque nous avons Promise, nous pouvons par exemple:

  • dĂ©finissez le rappel et obtenez ceux affected rowsqui nous reviennent PDOConnection;
  • gĂ©rer l'erreur, ajouter au journal;
  • Relancez la requĂȘte si le serveur SQL rĂ©pond avec une erreur.

$promise = $this->execAsync($sql, $user->data());

$promise->onResolve(function (int $rows) {
    echo "Affected rows: {$rows}";
});

$promise->onReject(function (\Throwable $error) {
    log($error->getMessage());
});

La question demeure: nous avons défini le rappel, et qui appellera resolveet reject?

Boucle d'événement


Il y a le concept de boucle d'Ă©vĂ©nement - une boucle d'Ă©vĂ©nement . Il est capable de traiter des messages dans un environnement asynchrone. Pour les E / S asynchrones, ce seront des messages du systĂšme d'exploitation que le socket est prĂȘt Ă  lire ou Ă  Ă©crire.

Comment ça fonctionne.

  • Le client indique Ă  Event Loop qu'il est intĂ©ressĂ© par une sorte de socket.
  • La boucle d'Ă©vĂ©nements interroge le systĂšme d'exploitation via un appel systĂšme stream_select: le socket est-il prĂȘt, toutes les donnĂ©es sont-elles Ă©crites, les donnĂ©es proviennent-elles de l'autre cĂŽtĂ©.
  • Si le systĂšme d'exploitation signale que le socket n'est pas prĂȘt, bloquĂ©, alors la boucle d'Ă©vĂ©nement rĂ©pĂšte la boucle.
  • Lorsque le systĂšme d'exploitation notifie que le socket est prĂȘt, la boucle d'Ă©vĂ©nements renvoie le contrĂŽle au client et active ( resolveou reject) Promise.



Nous exprimons ce concept dans le code: prenez le cas le plus simple, supprimez la gestion des erreurs et d'autres nuances, de sorte qu'il reste une boucle infinie. À chaque itĂ©ration, il interrogera le systĂšme d'exploitation sur les sockets qui sont prĂȘtes Ă  lire ou Ă  Ă©crire, et appellera un rappel pour une socket spĂ©cifique.

public static function run()
{
    while (true) {
        stream_select($readSockets, $writeSockets, null, 0);
        
        foreach ($readSockets as $i => $socket) {
            call_user_func(self::readCallbacks[$i], $socket);
        }

        // Do same for write sockets
    }
}

Nous complétons notre client SQL. Nous informons Event Loop que dÚs que les données du serveur SQL arrivent sur le socket avec lequel nous travaillons, nous devons mettre Deferred à l'état «terminé» et transférer les données du socket vers Promise.

public function execAsync(string $query, array $params = [])
{
    $deferred = new Deferred;
    ...
    Loop::onReadable($socket, function ($socket) use ($deferred) {
        $deferred->resolve(socket_read($socket));
    });

    return $deferred->promise();
}

La boucle d'événements peut gérer nos E / S et fonctionne avec des sockets . Que peut-il faire d'autre?

  • JavaScript setTimeout setInterval — . N . Event Loop .
  • Event Loop . process control, .

Event Loop


L'écriture de votre boucle d'événement est non seulement possible, mais également nécessaire. Si vous souhaitez travailler avec PHP asynchrone, il est important d'écrire votre propre implémentation simple pour comprendre comment cela fonctionne. Mais en production, nous ne l'utiliserons bien sûr pas, mais nous prendrons des implémentations toutes faites: stables, sans erreur et éprouvées au travail.

Il existe trois implémentations principales.

ReactPHP . Le plus ancien projet, a commencé en PHP 5.3. Maintenant, la version minimale requise de PHP est 5.3.8. Le projet implémente la norme Promises / A du monde JavaScript.

AMPHP . C'est cette implémentation que je préfÚre utiliser. La configuration minimale requise est PHP 7.0, et puisque la prochaine version est déjà 7.3. Il utilise des coroutines en plus de Promise.

Swoole. Il s'agit d'un cadre chinois intéressant dans lequel les développeurs tentent de porter certains concepts du monde Go vers PHP. La documentation en anglais est incomplÚte, la plupart sur GitHub en chinois. Si vous connaissez la langue, allez-y, mais jusqu'à présent, j'ai peur de travailler.



ReactPHP


Voyons Ă  quoi ressemblera le client en utilisant ReactPHP pour MySQL.

$connection = (new ConnectionFactory)->createLazyConnection();

$promise = $connection->query('UPDATE users SET ...');
$promise->then(
    function (QueryResult $command) {
        echo count($command->resultRows) . ' row(s) in set.';
    },
    function (Exception $error) {
        echo 'Error: ' . $error->getMessage();
    });

Tout est presque le mĂȘme que nous avons Ă©crit: nous crĂ©ons onnectionet exĂ©cutons la demande. Nous pouvons dĂ©finir le rappel pour traiter les rĂ©sultats (retour affected rows):

    function (QueryResult $command) {
        echo count($command->resultRows) . ' row(s) in set.';
    },

et rappel pour la gestion des erreurs:

    function (Exception $error) {
        echo 'Error: ' . $error->getMessage();
    });

À partir de ces rappels, vous pouvez crĂ©er des chaĂźnes longues-longues, car chaque rĂ©sultat thendans ReactPHP renvoie Ă©galement Promise.

$promise
    ->then(function ($data) {
        return new Promise(...);
    })
    ->then(function ($data) {
        ...
    }, function ($error) {
        log($error);
    })
    ...

Il s'agit d'une solution à un problÚme appelé enfer de rappel. Malheureusement, dans l'implémentation de ReactPHP, cela conduit au problÚme «Promise hell», lorsque des rappels 10-11 sont nécessaires pour connecter correctement RabbitMQ . Il est difficile de travailler avec un tel code et de le corriger. J'ai rapidement réalisé que ce n'était pas le mien et je suis passé à AMPHP.

Amphp


Ce projet est plus jeune que ReactPHP et promeut un concept diffĂ©rent - les coroutines . Si vous envisagez de travailler avec MySQL dans AMPHP, vous pouvez voir que c'est presque la mĂȘme chose que de travailler avec PDOConnectionPHP.

$pool = Mysql\pool("host=127.0.0.1 port=3306 db=test");

try {
    $result = yield $pool->query("UPDATE users SET ...");

    echo $result->affectedRows . ' row(s) in set.';
} catch (\Throwable $error) {
    echo 'Error: ' . $error->getMessage();
}

Ici, nous créons un pool, connectons et exécutons la demande. Nous pouvons gérer les erreurs via les erreurs habituelles try...catch, nous n'avons pas besoin de rappels.

Mais avant l'appel asynchrone, le mot-clé - apparaßt ici yield.

Générateurs


Le mot yield- clé transforme notre fonction en générateur.

function generator($counter = 1)
{
    yield $counter++;

    echo "A";

    yield $counter;

    echo "B";

    yield ++$counter;
}

DÚs que l'interpréteur PHP rencontre des yieldfonctions dans le corps, il se rend compte qu'il s'agit d'une fonction de générateur. Au lieu de s'exécuter, un objet classe est créé lors de son appel Generator.

Les générateurs héritent de l'interface de l'itérateur.

$generator = generator(1);

foreach ($generator as $value) {
    echo $value;
}

while ($generator->valid()) {
    echo $generator->current();

    $generator->next();
}

Par conséquent, il est possible d'exécuter des cycles foreachet whileet d' autres. Mais, plus intéressant, l'itérateur a des méthodes currentet next. Passons-les en revue étape par étape.

Exécutez notre fonction generator($counter = 1). Nous appelons la méthode du générateur current(). La valeur de la variable sera retournée $counter++.

DÚs que nous exécutons le générateur next(), le code ira au prochain appel à l'intérieur du générateur yield. L'ensemble du code entre les deux yields'exécutera, et c'est cool. En continuant de faire tourner le générateur, nous obtenons le résultat.

Coroutines


Mais le générateur a une fonction plus intéressante - nous pouvons envoyer des données au générateur de l'extérieur. Dans ce cas, ce n'est pas tout à fait un générateur, mais une coroutine ou une coroutine.

function printer() {  
    while (true) {     
        echo yield;       
    }                             
}                                

$print = printer();
$print->send('Hello');
$print->send(' PHPRussia');
$print->send(' 2019');
$print->send('!');

Dans cette section du code, il est intéressant de noter qu'il while (true)ne bloquera pas le flux d'exécution, mais sera exécuté une seule fois. Nous avons envoyé les données à Corutin et les avons reçues 'Hello'. Envoyé plus - reçu 'PHPRussia'. Le principe est clair.

En plus d'envoyer des données au générateur, vous pouvez envoyer des erreurs et les traiter de l'intérieur, ce qui est pratique.

function printer() {
    try {
        echo yield;
    } catch (\Throwable $e) {
        echo $e->getMessage();
    }
}

printer()->throw(new \Exception('Ooops...'));

RĂ©sumer. Corutin est un composant d'un programme qui prend en charge l'arrĂȘt et la poursuite de l'exĂ©cution tout en maintenant l'Ă©tat actuel . Corutin se souvient de sa pile d'appels, des donnĂ©es qu'il contient et peut les utiliser Ă  l'avenir.

Générateurs et promesse


Regardons le générateur et les interfaces Promise.

class Generator
{
    public function send($data);
    public function throw(\Throwable $error);
}

class Promise
{
    public function resolve($data);
    public function reject(\Throwable $error);
}

Ils se ressemblent, à l'exception des noms de méthode différents. Nous pouvons envoyer des données et envoyer une erreur au générateur et à Promise.

Comment cela peut-il ĂȘtre utilisĂ©? Écrivons une fonction.

function recoil(\Generator $generator)
{
    $promise = $generator->current();

    $promise->onResolve(function($data) use ($generator) {
        $generator->send($data);
        recoil($generator);
    };

    $promise->onReject(function ($error) use ($generator) {
        $generator->throw($error);
        recoil($generator);
    });
}

La fonction prend la valeur de courant du générateur: $promise = $generator->current();.

J'ai un peu exagéré. Oui, nous devons vérifier que la valeur actuelle qui nous est retournée est une sorte de instanceofpromesse. Si oui, alors nous pouvons lui demander un rappel. Il renvoie en interne les données au générateur lorsque Promise réussit et démarre récursivement la fonction recoil.

    $promise->onResolve(function($data) use ($generator) {
        $generator->send($data);
        recoil($generator);
    };

La mĂȘme chose peut ĂȘtre faite avec des erreurs. Si Promise a Ă©chouĂ©, par exemple, le serveur SQL a dit: «Trop de connexions», alors nous pouvons jeter l'erreur Ă  l'intĂ©rieur du gĂ©nĂ©rateur et passer Ă  l'Ă©tape suivante.

Tout cela nous amÚne au concept important du multitùche coopératif.

Multitùche coopératif


Il s'agit d'un type de multitĂąche, dans lequel la tĂąche suivante n'est effectuĂ©e qu'aprĂšs que la tĂąche en cours se dĂ©clare explicitement prĂȘte Ă  donner du temps processeur Ă  d'autres tĂąches.

Je rencontre rarement quelque chose de simple, comme travailler avec une seule base de données. Le plus souvent, dans le processus de mise à jour de l'utilisateur, vous devez mettre à jour les données dans la base de données, dans l'index de recherche, puis nettoyer ou mettre à jour le cache, puis envoyer 15 messages supplémentaires à RabbitMQ. En PHP, tout ressemble à ça.



Nous effectuons les opérations une par une: nous avons mis à jour la base de données, l'index, puis le cache. Mais par défaut, PHP bloque de telles opérations (E / S), donc si vous regardez attentivement, en fait, tout est ainsi.



Sur les parties sombres, nous avons bloqué. Ils prennent le plus de temps.

Si nous travaillons en mode asynchrone, alors ces parties ne sont pas là, la chronologie d'exécution est intermittente.



Vous pouvez tout coller ensemble et faire des piĂšces une par une.



À quoi tout cela sert-il? Si vous regardez la taille de la chronologie, au dĂ©but, cela prend beaucoup de temps, mais dĂšs que nous la collons ensemble, l'application accĂ©lĂšre.

Le concept mĂȘme de boucle d'Ă©vĂ©nement et de multitĂąche coopĂ©ratif a longtemps Ă©tĂ© utilisĂ© dans diverses applications: Nginx, Node.js, Memcached, Redis. Tous utilisent l'intĂ©rieur de la boucle d'Ă©vĂ©nements et sont construits sur le mĂȘme principe.

Depuis que nous avons commencĂ© Ă  parler des serveurs Web Nginx et Node.js, rappelons comment se dĂ©roule le traitement des requĂȘtes en PHP.

Traitement des demandes en PHP


Le navigateur envoie une requĂȘte, il arrive au serveur HTTP derriĂšre lequel se trouve un pool de flux FPM. L'un des threads met cette requĂȘte en service, connecte notre code et commence Ă  l'exĂ©cuter.



Lorsque la prochaine demande arrive, un autre thread FPM le récupérera, connectera le code et il sera exécuté.

Ce schéma de travail présente des avantages .

  • Gestion simple des erreurs . Si quelque chose a mal tournĂ© et qu'une des demandes est tombĂ©e, nous n'avons rien Ă  faire - la prochaine viendra, et cela n'affectera pas son travail.
  • Nous ne pensons pas Ă  la mĂ©moire . Nous n'avons pas besoin de nettoyer ou de surveiller la mĂ©moire. À la prochaine demande, toute la mĂ©moire sera effacĂ©e.

C'est un schéma sympa qui a fonctionné en PHP depuis le tout début et qui fonctionne toujours avec succÚs. Mais il y a aussi des inconvénients .

  • Limitez le nombre de processus . Si nous avons 50 threads FPM sur le serveur, dĂšs que la 51e requĂȘte arrive, il attendra que l'un des threads devienne libre.
  • CoĂ»ts pour le changement de contexte . Le systĂšme d'exploitation bascule les demandes entre les flux FPM. Cette opĂ©ration au niveau du processeur est appelĂ©e Context Switch. Il coĂ»te cher et exĂ©cute un grand nombre de mesures. Il faut sauvegarder tous les registres, la pile d'appels, tout ce qui se trouve dans le processeur, puis passer Ă  un autre processus, charger ses registres et sa pile d'appels, y refaire quelque chose, basculer Ă  nouveau, sauvegarder encore ... Longtemps.

Abordons la question diffĂ©remment - nous Ă©crirons un serveur HTTP en PHP lui-mĂȘme.

Serveur HTTP asynchrone




Ça peut ĂȘtre fait. Nous avons dĂ©jĂ  appris Ă  travailler avec des sockets en mode non bloquant, et une connexion HTTP est la mĂȘme socket. À quoi ressemblera-t-il et fonctionnera-t-il?

Ceci est un exemple de démarrage de serveurs HTTP dans le cadre AMPHP.

Loop::run(function () {
    $app = new Application();
    $app->bootstrap();

    $sockets = [Socket\listen('0.0.0.0:80')];

    $server = new Server($sockets, new CallableRequestHandler(
        function (Request $request) use ($app) {
            $response = yield $app->dispatch($request);

            return new Response(Status::OK, [], $response);
        })
    );

    yield $server->start();
});

Tout est assez simple: charger Applicationet créer un pool de sockets (un ou plusieurs).

Ensuite, nous démarrons notre serveur, le configurons Handler, qui sera exécuté à chaque demande et envoyons la demande à la nÎtre Applicationafin d'obtenir une réponse.

La derniÚre chose à faire est de démarrer le serveur yield $server->start();.

Dans ReactPHP, il aura Ă  peu prĂšs la mĂȘme apparence, mais il n'y aura que 150 rappels pour diffĂ©rentes options, ce qui n'est pas trĂšs pratique.

ProblĂšmes


Il y a plusieurs problĂšmes avec l'asynchronie en PHP.

Manque de normes . Chaque framework: Swoole, ReactPHP ou AMPHP, implémente sa propre interface Promise et ils sont incompatibles.

AMPHP pourrait théoriquement interagir avec Promise de ReactPHP, mais il y a une mise en garde. Si le code de ReactPHP n'est pas trÚs bien écrit, et quelque part appelle ou crée implicitement une boucle d'événement, il s'avÚre que deux boucles d'événement tourneront à l'intérieur.

JavaScript a une norme Promises / A + relativement bonne qui implémente Guzzle. Ce serait bien si les frameworks le suivaient. Mais jusqu'à présent, ce n'est pas le cas.

Fuites de mĂ©moire. Lorsque nous travaillons en PHP dans le mode FPM habituel, nous ne pensons peut-ĂȘtre pas Ă  la mĂ©moire. MĂȘme si les dĂ©veloppeurs d'une extension ont oubliĂ© d'Ă©crire du bon code, ont oubliĂ© d'exĂ©cuter Valgrind et quelque part Ă  l'intĂ©rieur de la mĂ©moire, alors ça va - la prochaine demande sera effacĂ©e et recommencera. Mais en mode asynchrone, vous ne pouvez pas vous le permettre, car tĂŽt ou tard, nous tomberons simplement OutOfMemoryException.

Il est possible de réparer, mais c'est difficile et douloureux. Dans certains cas, Xdebug aide, dans d'autres, à analyser les erreurs qui ont causé OutOfMemoryException.

Opérations de blocage . Il est essentiel de ne pas bloquer la boucle d'événements lorsque nous écrivons du code asynchrone. L'application ralentit dÚs que nous bloquons le flux d'exécution, chacune de nos coroutines commence à s'exécuter plus lentement.

Le paquetage kelunik / loop-block aidera Ă  trouver de telles opĂ©rations pour AMPHP . Il rĂšgle la minuterie sur un trĂšs petit intervalle. Si la minuterie ne fonctionne pas, nous sommes bloquĂ©s quelque part. Le package aide Ă  trouver des emplacements de blocage, mais pas toujours: le blocage dans certaines extensions peut ne pas ĂȘtre remarquĂ©.

Prise en charge de la bibliothĂšque: Cassandra, Influx, ClickHouse . Le principal problĂšme de tout PHP asynchrone est le support des bibliothĂšques. Nous ne pouvons pas utiliser les habituels PDOConnection, les RedisClientpilotes d' autres pour tout le monde - nous avons besoin d' implĂ©mentations non-bloquant. Ils doivent Ă©galement ĂȘtre Ă©crits en PHP en mode non bloquant, car les pilotes C fournissent rarement des interfaces pouvant ĂȘtre intĂ©grĂ©es dans du code asynchrone.

L'expĂ©rience la plus Ă©trange que j'ai eue avec le pilote de la base de donnĂ©es Cassandra. Ils fournissent des opĂ©rationsExecuteAsync, GetAsyncet d'autres, mais en mĂȘme temps, ils retournent un objet Futureavec une seule mĂ©thode getqui bloque. Il est possible d'obtenir quelque chose de maniĂšre asynchrone, mais pour attendre le rĂ©sultat, nous bloquerons toujours toute notre boucle. Pour le faire diffĂ©remment, par exemple, par le biais de rappels, cela ne fonctionne pas. J'ai mĂȘme Ă©crit mon client pour Cassandra, car nous l'utilisons dans notre travail.

Indication de type . C'est un problĂšme d'AMPHP et de corutine.

class UserRepository
{
    public function find(int $id): \Generator
    {
        $data = yield $this->db->query('SELECT ...', $id);

        return User::fill($data);
    }
}

S'il se produit dans une fonction yield, il devient alors un gĂ©nĂ©rateur. À ce stade, nous ne pouvons plus spĂ©cifier les types de donnĂ©es de retour corrects.

PHP 8


Qu'est-ce qui nous attend en PHP 8? Je vais vous parler de mes hypothÚses ou plutÎt de mes envies ( NDLR: Dmitry Stogov sait ce qui va réellement apparaßtre en PHP 8 ).

Boucle d'événement Il y a une chance qu'il apparaisse, car des travaux sont en cours pour apporter une boucle d'événement sous une forme quelconque au noyau. Si cela se produit, nous aurons une fonction await, comme en JavaScript ou C #, qui nous permettra d'attendre le résultat de l'opération asynchrone à un certain endroit. Dans ce cas, nous n'aurons pas besoin d'extensions, tout fonctionnera de maniÚre asynchrone au niveau du noyau.


class UserRepository
{
    public function find(int $id): Promise<User>
    {
        $data = await $this->db->query('SELECT ...', $id);

        return User::fill($data);
    }
}


Génériques Go attend les génériques, nous attendons les génériques, tout le monde attend les génériques.

class UserRepository
{
    public function find(int $id): Promise<User>
    {
        $data = yield $this->db->query('SELECT ...', $id);

        return User::fill($data);
    }
}

Mais nous n'attendons pas Generics pour les collections, mais pour indiquer que le résultat de Promise sera exactement l'objet User.

Pourquoi tout ça?

Pour la vitesse et les performances.
PHP est un langage dans lequel la plupart des opérations sont liées aux E / S. Nous écrivons rarement du code qui est significativement lié aux calculs dans le processeur. TrÚs probablement, nous travaillons avec des sockets: nous devons faire une demande à la base de données, lire quelque chose, retourner une réponse, envoyer un fichier. L'asynchronie vous permet d'accélérer un tel code. Si nous regardons le temps de réponse moyen pour 1 000 demandes, nous pouvons accélérer d'environ 8 fois et de 10 000 demandes de prÚs de 6!

Le 13 mai 2020, nous nous réunirons pour la deuxiÚme fois à PHP Russie pour discuter du langage, des bibliothÚques et des cadres, des façons d'augmenter la productivité et des piÚges des solutions de battage médiatique. Nous avons accepté les 4 premiers rapports , mais l'appel à communications arrive toujours. Postulez si vous souhaitez partager votre expérience avec la communauté.

Source: https://habr.com/ru/post/undefined/


All Articles