PHP 8.1 est sorti voilà bientôt un an avec son lot de nouveautés : énumérations, propriétés en lecture seule, nouvelle façon d’initialiser un argument, etc.

Lorsque l’on cite les nouveautés de cette version 8.1, ce sont souvent les fonctionnalités listées précédemment qui reviennent, et rarement « fibers ». Elle n’en reste pas moins l’un des ajouts majeurs.

Cet article a pour objectif d’expliquer ce que sont les fibers, comment elles fonctionnent et quelles sont leurs utilités. Avant de rentrer dans le vif du sujet, il est important de rappeler quelques fondamentaux.

Code synchrone et asynchrone

Si vous êtes un pur développeur PHP il y a peu de chances pour que vous ayez déjà été confronté à de l’asynchronisme, le langage n’ayant pas été conçu pour ça.

On associe souvent dans le monde du web l’exécution de code asynchrone à JavaScript, d’abord avec l’utilisation des callbacks, puis des promises et plus récemment celle des fonctions “async” et “await” (qui ne sont rien d’autre que des abstractions des callbacks).

Cette association est couramment faite parce que le modèle de conception de JavaScript repose sur un mécanisme d'”event loop” qui permet d’exécuter du code asynchrone.

La précédence du code asynchrone

Pour ceux qui ne sont pas familiers avec ce concept, les deux sections suivantes le résument brièvement.

Programmation synchrone

La programmation synchrone est le paradigme le plus courant. Il consiste en l’exécution du code de manière séquentielle, c’est-à-dire instruction après instruction.

<?php

function getProductData(HttpClientInterface $httpClient, int $productId): array
{
    $metadata = $httpClient->request('GET', 'https://product-metadata/api/' . $productId)->getContent();
    $stock = $httpClient->request('GET', 'https://product-stock/api/' . $productId)->getContent();

    return ['metadata' => $metadata, 'stock' => $stock];
}

L’exécution de la fonction getProductData va effectuer deux appels HTTP séquentiellement, l’API de gestion du stock sera appelée une fois que l’API des métadonnées aura retourné une réponse.

Si le premier appel prend deux secondes et que le second prend une seconde, le temps d’exécution total sera de trois secondes.

Programmation asynchrone

La programmation asynchrone consiste à exécuter plusieurs instructions sans attendre que la précédente soit terminée pour exécuter la suivante.

Transformons l’exemple précédent en asynchrone à l’aide de pseudo code :

<?php

function getProductData(HttpClientInterface $httpClient, int $productId): array
{
    $metadataPromise = get('https://product-metadata/api/' . $productId);
    $stockPromise = get('https://product-stock/api/' . $productId);

    [$metadata, $stock] = await all([$metadataPromise, $stockPromise]);
    
    return ['metadata' => $metadata, 'stock' => $stock];    
}

Les deux appels HTTP sont faits de manière asynchrone, le premier appel prend toujours deux secondes pour retourner une réponse, mais le second appel n’attend pas que le premier soit terminé pour débuter.

Au lieu de durer trois secondes, le temps total d’exécution de getProductData est donc de deux secondes.

Sans rentrer dans les détails d’implémentation des Promises, le code diffère, car il faut s’assurer que les deux appels soient terminés avant de retourner une valeur (il faut donc attendre).

PHP Fibers, qu’est-ce que c’est ?

Maintenant que les concepts précédents ont été remémorés, on peut se pencher sur fibers.

Fibers est un nouveau mécanisme permettant d’interrompre une ou plusieurs fonctions. Cela se traduit par l’ajout d’une nouvelle classe « Fiber » :

final class Fiber {
	/* Methods */
	public __construct(callable $callback)
	public start(mixed ...$args): mixed
	public resume(mixed $value = null): mixed
	public throw(Throwable $exception): mixed
	public getReturn(): mixed
	public isStarted(): bool
	public isSuspended(): bool
	public isRunning(): bool
	public isTerminated(): bool
	public static suspend(mixed $value = null): mixed
	public static getCurrent(): ?Fiber
}

L’interface propose des méthodes logiques au regard de son utilité : démarrer et suspendre l’exécution de fonctions.

Comment ça marche

Prenons l’exemple le plus basique tiré de la documentation (avec un léger renommage des variables pour gagner en compréhension) pour que cela soit plus concret :

<?php
$fiber = new Fiber(function (): void {
   $resumedValue = Fiber::suspend('fiber');
   echo "Value used to resume fiber: ", $resumedValue, PHP_EOL;
});

$suspendedValue = $fiber->start();

echo "Value from fiber suspending: ", $suspendedValue, PHP_EOL;

$fiber->resume('test');

Cet exemple va afficher :

Value from fiber suspending: fiber
Value used to resume fiber: test

Avec du code synchrone, on s’attendrait à ce que l’instruction echo de la fonction anonyme soit affichée avant le second echo à la ligne 11. Cependant, les fibers permettant de suspendre l’exécution du callable en paramètre de fonction grâce à l’invocation de la méthode Fiber::suspend(), à peine démarrée, celui-ci s’arrête.

Jusqu’ici, pas grand-chose d’utilisable. Ça devient plus parlant lorsqu’une fiber est imbriquée dans une autre :

<?php
$parent = new Fiber(function () {
    echo "I am...", PHP_EOL;
    
    $child = new Fiber(function () {
        echo "No... No...", PHP_EOL;
        Fiber::suspend();
        echo "It's not true... That's impossible!", PHP_EOL;
    });
    
    $child->start();
    
    echo "...your father!", PHP_EOL;
    
    return $child;
});

$parent->start();
$child = $parent->getReturn();
$child->resume();

Il est possible de démarrer une fiber enfant et de contrôler son exécution en dehors du fiber parent. Le code ci-dessus retourne :

I am... 
No... No... 
...your father! 
It's not true... That's impossible!

Mais où est l’asynchrone ?

Vous avez peut-être déjà lu quelques articles sur fibers parlant d’asynchronisme. Cet article commence d’ailleurs par une brève explication du fonctionnement de code asynchrone. Le moment est donc venu de montrer des exemples de code asynchrone en PHP avec fibers.

C’est là que vous risquez d’être déçu. Il y a un manque de compréhension, ou de clarté, autour de cette nouvelle fonctionnalité. Comme dit précédemment :

Fibers est un nouveau mécanisme permettant d’interrompre une ou plusieurs fonctions.

Et rien de plus !

Alors pourquoi les gens associent PHP fibers avec l’asynchrone ? Parce que couplé avec une “event loop” (ça ne vous rappelle rien ?) c’est faisable.

Pour reprendre l’exemple de code asynchrone en début d’article, vous auriez deux fibers qui envoient chacune une requête HTTP puis se mettent en pause. Vous boucleriez dessus (l’event loop) en reprenant l’exécution de chacune d’entre elles pour vérifier si la réponse HTTP est complète. Si ce n’est pas le cas, vous mettriez à nouveau en pause chaque fiber pour passer à la suivante, et ainsi de suite.

Pour autant, ce n’est pas réellement du code asynchrone. C’est vous qui gérez l’exécution de chaque fiber, quand la stopper et quand la reprendre. C’est ce qu’on appelle des “green threads“, des sortes de threads qui sont exécutés dans le même processus et gérés par le runtime (ici le runtime PHP).

J’avais commencé à développer un exemple démontrant comment effectuer des requêtes HTTP asynchrone, mais c’est loin d’être trivial. Une fois que vous avez votre boucle avec les fibers qui se stoppent et redémarrent, vous devez ensuite implémenter votre propre client HTTP à l’aide de la fonction stream_socket_client(). C’est votre seule possibilité pour réellement avoir des appels non bloquants.

Vous pouvez trouver une implémentation concrète à l’adresse suivante : https://github.com/nox7/async-php-8-io-http.

Conclusion

Habituellement, lorsque je rédige un nouvel article technique, le sujet abordé est basé sur mon expérience et mes connaissances. Pour celui-ci, j’ai pris le parti inverse : choisir un sujet que j’ai envie de découvrir au travers de la rédaction d’un article de blog.

J’ai opté pour PHP fibers car j’avais beau lire sa description dans la liste des changements de PHP 8.1 son utilité et les concepts associés étaient pour moi flous.

Il ne m’a pas été simple de synthétiser le plus simplement possible son utilité puisque pour comprendre quel est son réel intérêt, il faut assimiler ce qu’implique l’asynchronisme, ce que sont les green threads ou encore les coroutines. Plus je voulais rendre les choses claires, plus je me perdais dans un nouveau concept qui m’était inconnu.

Pour résumer ce que j’ai compris du sujet, je dirais que PHP fibers est un mécanisme permettant de normaliser la mise en place de code asynchrone. C’est une fonctionnalité avancée, destinée aux développeurs de frameworks et librairies et leur offrant la possibilité d’intégrer du code asynchrone dans du code synchrone de façon transparente.

Si vous voulez faire de l’asynchrone avec PHP, je vous conseille d’utiliser AMPHP ou ReactPHP, qui n’ont pas attendu l’arrivée de fiber, mais qui en tirent maintenant parti.

Pour aller plus loin

Voici pêle-mêle une liste de liens sur l’asynchrone, fibers, les coroutines, etc. Si vous voulez aller plus loin sur le sujet :