AMPHP est une collection de bibliothèques événementielles pour PHP conçues avec les fibres et la concurrence à l'esprit. amphp/amp
fournit spécifiquement des contrats à terme et des annulations comme primitives fondamentales pour la programmation asynchrone. Nous utilisons maintenant Revolt au lieu de livrer une implémentation de boucle d'événements avec amphp/amp
.
Amp utilise largement les fibres fournies avec PHP 8.1 pour écrire du code asynchrone, tout comme le code synchrone et bloquant. Contrairement aux versions précédentes, il n'est pas nécessaire de recourir à des coroutines ou à des rappels basés sur un générateur. Semblable aux threads, chaque fibre possède sa propre pile d'appels, mais les fibres sont planifiées de manière coopérative par la boucle d'événements. Utilisez Ampasync()
pour exécuter les choses simultanément.
Traditionnellement, PHP suit un modèle d'exécution séquentielle. Le moteur PHP exécute une ligne après l’autre dans un ordre séquentiel. Cependant, les programmes sont souvent constitués de plusieurs sous-programmes indépendants qui peuvent être exécutés simultanément.
Si vous interrogez une base de données, vous envoyez la requête et attendez la réponse du serveur de base de données de manière bloquante. Une fois que vous avez la réponse, vous pouvez commencer à passer à l’étape suivante. Au lieu de rester là et de ne rien faire en attendant, nous pourrions déjà envoyer la prochaine requête de base de données ou effectuer un appel HTTP vers une API. Profitons du temps que nous passons habituellement à attendre les E/S !
Revolt permet de telles opérations d'E/S simultanées. Nous maintenons la charge cognitive faible en évitant les rappels. Nos API peuvent être utilisées comme n'importe quelle autre bibliothèque, sauf que les choses fonctionnent également simultanément, car nous utilisons des E/S non bloquantes sous le capot. Exécutez les choses simultanément en utilisant Ampasync()
et attendez le résultat en utilisant Future::await()
où et quand vous en avez besoin !
Il y a eu diverses techniques pour implémenter la concurrence en PHP au fil des années, par exemple les rappels et les générateurs livrés avec PHP 5. Ces approches ont souffert du problème "De quelle couleur est votre fonction", que nous avons résolu en livrant Fibers avec PHP 8.1. Ils permettent la concurrence avec plusieurs piles d'appels indépendantes.
Les fibres sont planifiées de manière coopérative par la boucle d'événements, c'est pourquoi elles sont également appelées coroutines. Il est important de comprendre qu'une seule coroutine est en cours d'exécution à un moment donné, toutes les autres coroutines sont suspendues entre-temps.
Vous pouvez comparer les coroutines à un ordinateur exécutant plusieurs programmes utilisant un seul cœur de processeur. Chaque programme dispose d'un créneau horaire à exécuter. Les coroutines, cependant, ne sont pas préventives. Ils ne bénéficient pas de leur créneau horaire fixe. Ils doivent volontairement abandonner le contrôle de la boucle événementielle.
Toute fonction d'E/S bloquante bloque l'ensemble du processus en attendant les E/S. Vous voudrez les éviter. Si vous n'avez pas lu le guide d'installation, jetez un œil à l'exemple Hello World qui démontre l'effet du blocage des fonctions. Les bibliothèques fournies par AMPHP évitent le blocage des E/S.
Ce package peut être installé en tant que dépendance de Composer.
composer require amphp/amp
Si vous utilisez cette bibliothèque, il est très probable que vous souhaitiez planifier des événements à l'aide de Revolt, dont vous devriez avoir besoin séparément, même si elle est automatiquement installée en tant que dépendance.
composer require revolt/event-loop
Ces packages fournissent les éléments de base pour les applications asynchrones/simultanées en PHP. Nous proposons de nombreux packages s'appuyant sur ceux-ci, par exemple
amphp/byte-stream
fournissant une abstraction de fluxamphp/socket
fournissant une couche de socket pour UDP et TCP, y compris TLSamphp/parallel
fournissant un traitement parallèle pour utiliser plusieurs cœurs de processeur et décharger les opérations de blocageamphp/http-client
fournissant un client HTTP/1.1 et HTTP/2amphp/http-server
fournissant un serveur d'applications HTTP/1.1 et HTTP/2amphp/mysql
et amphp/postgres
pour un accès non bloquant à la base de donnéesCe package nécessite PHP 8.1 ou version ultérieure. Aucune extension requise !
Les extensions ne sont nécessaires que si votre application nécessite un nombre élevé de connexions socket simultanées. Cette limite est généralement configurée jusqu'à 1 024 descripteurs de fichiers.
Les coroutines sont des fonctions interruptibles. En PHP, ils peuvent être implémentés à l'aide de fibres.
Remarque Les versions précédentes d'Amp utilisaient des générateurs dans un but similaire, mais les fibres peuvent être interrompues n'importe où dans la pile d'appels, rendant inutile le passe-partout précédent comme
Ampcall()
.
À un moment donné, une seule fibre fonctionne. Lorsqu'une coroutine est suspendue, son exécution est temporairement interrompue, permettant ainsi l'exécution d'autres tâches. L'exécution reprend une fois qu'un minuteur expire, que les opérations de flux sont possibles ou que tout Future
attendu est terminé.
La suspension de bas niveau et la reprise des coroutines sont gérées par l'API Suspension
de Revolt.
<?php
require __DIR__ . ' /vendor/autoload.php ' ;
use Revolt EventLoop ;
$ suspension = EventLoop:: getSuspension ();
EventLoop:: delay ( 5 , function () use ( $ suspension ): void {
print ' ++ Executing callback created by EventLoop::delay() ' . PHP_EOL ;
$ suspension -> resume ( null );
});
print ' ++ Suspending to event loop... ' . PHP_EOL ;
$ suspension -> suspend ();
print ' ++ Script end ' . PHP_EOL ;
Les rappels enregistrés sur la boucle d'événements Revolt sont automatiquement exécutés en tant que coroutines et il est possible de les suspendre en toute sécurité. Outre l'API de boucle d'événements, Ampasync()
peut être utilisé pour démarrer une pile d'appels indépendante.
<?php
use function Amp delay ;
require __DIR__ . ' /vendor/autoload.php ' ;
Amp async ( function () {
print ' ++ Executing callback passed to async() ' . PHP_EOL ;
delay ( 3 );
print ' ++ Finished callback passed to async() ' . PHP_EOL ;
});
print ' ++ Suspending to event loop... ' . PHP_EOL ;
delay ( 5 );
print ' ++ Script end ' . PHP_EOL ;
Un Future
est un objet représentant le résultat éventuel d’une opération asynchrone. Il existe trois états :
Un futur complété avec succès est analogue à une valeur de retour, tandis qu'un futur erroné est analogue à la levée d'une exception.
Une façon d'aborder les API asynchrones consiste à utiliser des rappels qui sont transmis lorsque l'opération est démarrée et appelée une fois qu'elle est terminée :
doSomething ( function ( $ error , $ value ) {
if ( $ error ) {
/* ... */
} else {
/* ... */
}
});
L'approche de rappel présente plusieurs inconvénients.
C'est là que l'avenir entre en jeu. Ce sont des espaces réservés pour le résultat qui sont renvoyés comme toute autre valeur de retour. L'appelant a le choix d'attendre le résultat en utilisant Future::await()
ou d'enregistrer un ou plusieurs rappels.
try {
$ value = doSomething ()-> await ();
} catch (...) {
/* ... */
}
Dans les applications simultanées, il y aura plusieurs futurs, où vous souhaiterez peut-être les attendre tous ou seulement le premier.
AmpFutureawait($iterable, $cancellation)
attend tous Future
objets d'un iterable
. Si l’une des instances Future
échoue, l’opération sera abandonnée à cette exception près. Sinon, le résultat est un tableau correspondant aux clés de l' iterable
d'entrée à leurs valeurs d'achèvement.
Le combinateur await()
est extrêmement puissant car il vous permet d'exécuter simultanément de nombreuses opérations asynchrones. Regardons un exemple utilisant amphp/http-client
pour récupérer plusieurs ressources HTTP simultanément :
<?php
use Amp Future ;
use Amp Http Client HttpClientBuilder ;
use Amp Http Client Request ;
$ httpClient = HttpClientBuilder:: buildDefault ();
$ uris = [
" google " => " https://www.google.com " ,
" news " => " https://news.google.com " ,
" bing " => " https://www.bing.com " ,
" yahoo " => " https://www.yahoo.com " ,
];
try {
$ responses = Future await ( array_map ( function ( $ uri ) use ( $ httpClient ) {
return Amp async ( fn () => $ httpClient -> request ( new Request ( $ uri , ' HEAD ' )));
}, $ uris ));
foreach ( $ responses as $ key => $ response ) {
printf (
" %s | HTTP/%s %d %s n" ,
$ key ,
$ response -> getProtocolVersion (),
$ response -> getStatus (),
$ response -> getReason ()
);
}
} catch ( Exception $ e ) {
// If any one of the requests fails the combo will fail
echo $ e -> getMessage (), "n" ;
}
AmpFutureawaitAnyN($count, $iterable, $cancellation)
est identique à await()
sauf qu'il tolère les erreurs individuelles. Un résultat est renvoyé une fois exactement $count
instances dans l' iterable
terminé avec succès. La valeur de retour est un tableau de valeurs. Les clés individuelles du tableau de composants sont préservées de l' iterable
transmis à la fonction pour évaluation.
AmpFutureawaitAll($iterable, $cancellation)
attend tous les contrats à terme et renvoie leurs résultats sous forme de tableau [$errors, $values]
.
AmpFutureawaitFirst($iterable, $cancellation)
déballe le premier Future
terminé, qu'il soit terminé avec succès ou erroné.
AmpFutureawaitAny($iterable, $cancellation)
déballe le premier Future
terminé avec succès.
Les futurs peuvent être créés de plusieurs manières. La plupart du code utilisera Ampasync()
qui prend une fonction et l'exécute comme coroutine dans une autre fibre.
Parfois, une interface exige qu'un Future
soit renvoyé, mais les résultats sont immédiatement disponibles, par exemple parce qu'ils sont mis en cache. Dans ces cas, Future::complete(mixed)
et Future::error(Throwable)
peuvent être utilisés pour construire un Future
immédiatement terminé.
Remarque L'API
DeferredFuture
décrite ci-dessous est une API avancée dont de nombreuses applications n'ont probablement pas besoin. Utilisez plutôtAmpasync()
ou des combinateurs lorsque cela est possible.
AmpDeferredFuture
est chargé de terminer un Future
en attente. Vous créez un AmpDeferredFuture
et utilisez sa méthode getFuture
pour renvoyer un AmpFuture
à l'appelant. Une fois le résultat prêt, vous complétez le Future
détenu par l'appelant en utilisant complete
ou error
sur le DeferredFuture
lié.
final class DeferredFuture
{
public function getFuture (): Future ;
public function complete ( mixed $ value = null );
public function error ( Throwable $ throwable );
}
Avertissement Si vous transmettez des objets
DeferredFuture
, vous faites probablement quelque chose de mal. Ils sont censés être l’état interne de votre opération.
Avertissement Vous ne pouvez pas compléter un futur avec un autre futur ; Utilisez
Future::await()
avant d'appelerDeferredFuture::complete()
dans de tels cas.
Voici un exemple simple d'un producteur de valeur asynchrone asyncMultiply()
créant un DeferredFuture
et renvoyant le Future
associé à son appelant.
<?php // Example async producer using DeferredFuture
use Revolt EventLoop ;
function asyncMultiply ( int $ x , int $ y ): Future
{
$ deferred = new Amp DeferredFuture ;
// Complete the async result one second from now
EventLoop:: delay ( 1 , function () use ( $ deferred , $ x , $ y ) {
$ deferred -> complete ( $ x * $ y );
});
return $ deferred -> getFuture ();
}
$ future = asyncMultiply ( 6 , 7 );
$ result = $ future -> await ();
var_dump ( $ result ); // int(42)
Chaque opération prenant en charge l'annulation accepte une instance de Cancellation
comme argument. Les annulations sont des objets qui permettent aux gestionnaires d'enregistrement de s'abonner aux demandes d'annulation. Ces objets sont transmis aux sous-opérations ou doivent être gérés par l'opération elle-même.
$cancellation->throwIfRequested()
peut être utilisé pour faire échouer l'opération en cours avec une CancelledException
une fois l'annulation demandée. Bien que throwIfRequested()
fonctionne bien, certaines opérations peuvent vouloir s'abonner avec un rappel à la place. Ils peuvent le faire en utilisant Cancellation::subscribe()
pour souscrire à toute demande d'annulation qui pourrait survenir.
L'appelant crée une Cancellation
en utilisant l'une des implémentations ci-dessous.
Remarque Les annulations sont à titre indicatif uniquement. Un résolveur DNS peut ignorer les demandes d'annulation une fois la requête envoyée, car la réponse doit de toute façon être traitée et peut toujours être mise en cache. Un client HTTP peut poursuivre une requête HTTP presque terminée pour réutiliser la connexion, mais peut abandonner une réponse de codage fragmentée car il ne peut pas savoir si continuer est réellement moins cher que d'abandonner.
Un TimeoutCancellations
s'annule automatiquement après le nombre de secondes spécifié.
request ( " ... " , new Amp TimeoutCancellation ( 30 ));
Un SignalCancellation
s'annule automatiquement après qu'un signal spécifié a été reçu par le processus en cours.
request ( " ... " , new Amp SignalCancellation ( SIGINT ));
Un DeferredCancellation
permet une annulation manuelle avec l’appel d’une méthode. C'est la méthode préférée si vous devez enregistrer un rappel personnalisé quelque part au lieu d'expédier votre propre implémentation. Seul l'appelant a accès à DeferredCancellation
et peut annuler l'opération à l'aide DeferredCancellation::cancel()
.
$ deferredCancellation = new Amp DeferredCancellation ();
// Register some custom callback somewhere
onSomeEvent ( fn () => $ deferredCancellation -> cancel ());
request ( " ... " , $ deferredCancellation -> getCancellation ());
Une NullCancellation
ne sera jamais annulée. L'annulation est souvent facultative, ce qui est généralement implémenté en rendant le paramètre nullable. Pour éviter les gardes comme if ($cancellation)
, une NullCancellation
peut être utilisée à la place.
$ cancellation ??= new NullCancellationToken ();
Un CompositeCancellation
combine plusieurs objets d’annulation indépendants. Si l’une de ces annulations est annulée, la CompositeCancellation
elle-même sera annulée.
amphp/amp
suit la spécification de gestion de versions sémantique Semver comme tous les autres packages amphp
.
Les packages compatibles doivent utiliser le sujet amphp
sur GitHub.
Si vous découvrez des problèmes liés à la sécurité, veuillez m'envoyer un e-mail [email protected]
au lieu d'utiliser le suivi des problèmes.
La licence MIT (MIT). Veuillez consulter LICENSE
pour plus d'informations.