Multiplexage de Fibers

OxPHP utilise les Fibers de PHP pour traiter plusieurs requêtes HTTP simultanément sur un seul thread worker. Lorsqu'une requête appelle oxphp_sleep() ou oxphp_async_await() (lorsque le pool asynchrone est activé), elle se suspend et le thread worker prend immédiatement en charge la requête suivante. Un seul worker peut gérer des centaines de requêtes en cours sans threads supplémentaires. Quelques contextes font exception ; ils sont énumérés sous Points de suspension.

Fonctionnement

L'ordonnanceur exécute de nombreuses requêtes sur un seul thread, en attribuant à chacune sa propre Fiber et en basculant de l'une à l'autre chaque fois qu'une Fiber se suspend.

  1. Une requête arrive et l'ordonnanceur l'assigne à une Fiber : un contexte d'exécution léger doté de sa propre pile et de son propre état PHP.
  2. La Fiber exécute le gestionnaire oxphp_worker(). Si le gestionnaire se termine sans se suspendre, la réponse est envoyée et la Fiber est recyclée sans surcoût par rapport à un worker à requête unique.
  3. Si le gestionnaire appelle une fonction suspensive (oxphp_sleep(), oxphp_usleep(), oxphp_async_await()), la Fiber rend la main à l'ordonnanceur.
  4. L'ordonnanceur prend en charge les nouvelles requêtes entrantes (en créant de nouvelles Fibers) et reprend les Fibers suspendues dont les conditions d'attente sont satisfaites (minuterie expirée, résultat asynchrone prêt).
  5. L'état PHP de chaque Fiber ($_GET, $_POST, $_COOKIE, $_SERVER, $_FILES, $_REQUEST, en-têtes de réponse, tampons de sortie, pile de la VM) est sauvegardé lors de la suspension et restauré à la reprise : une Fiber qui reprend retrouve ainsi son propre état, et non celui de ce qui s'est exécuté entre-temps.
graph LR
  W["Worker Thread"]
  W --> A0["Fiber A: handling /api/users"]
  A0 --> A1["oxphp_sleep(0.5)"]
  A1 --> A2["suspended"]
  A2 --> A3["resumed"]
  A3 --> A4["response"]
  W --> B0["Fiber B: handling /api/orders"]
  B0 --> B1["oxphp_async_await($p)"]
  B1 --> B2["suspended"]
  B2 --> B3["resumed"]
  B3 --> B4["response"]
  W --> C0["Fiber C: handling /health"]
  C0 --> C1["response (no suspension, zero overhead)"]
Tout n'est pas mis de côté par Fiber

Tout ce qui ne figure pas dans cette liste est partagé par les requêtes qui se chevauchent sur un thread. La session est l'élément à connaître nommément : $_SESSION, l'identifiant de session et la session ouverte qui les sous-tend n'existent qu'en un seul exemplaire par thread worker. Une requête que l'ordonnanceur admet à côté d'une requête suspendue ayant laissé une session ouverte reçoit cette session plutôt que la sienne, et session_write_close() ne les sépare pas (voir Mode worker → État de la session). $_ENV va plus loin : c'est par conception un état du worker, donc une écriture qu'une requête y fait est lue par toutes les requêtes suivantes sur ce worker, qu'elles se chevauchent ou non (voir Superglobales).

Configuration

Le multiplexage de Fibers s'active automatiquement lorsque le mode worker est activé. Aucune variable d'environnement supplémentaire n'est nécessaire.

Variable Par défaut Description
WORKER_MODE_ENABLED false Définissez sur true et faites pointer ENTRY_FILE vers un bootstrap .php pour activer le mode worker et le multiplexage de Fibers
PHP_WORKERS CPU / 2 (min 1) Nombre de threads worker. Chaque thread exécute son propre ordonnanceur indépendant, avec jusqu'à 256 Fibers concurrentes

Le nombre maximal de Fibers concurrentes par thread worker est de 256. Avec 4 threads worker, OxPHP peut traiter jusqu'à 1 024 requêtes en cours simultanément.

Points de suspension

Ces fonctions suspendent la Fiber courante et permettent à d'autres requêtes de s'exécuter sur le même thread :

Fonction Ce qui se passe
oxphp_sleep(float $seconds) Suspend la Fiber pendant la durée indiquée. Les autres Fibers continuent de s'exécuter, sauf dans les contextes énumérés dans la note ci-dessous, où l'appel attend plutôt sur le thread worker
oxphp_usleep(int $microseconds) Identique à oxphp_sleep() avec une granularité à la microseconde (minimum 1 ms)
oxphp_async_await(int $promise_id) Suspend la Fiber jusqu'à ce que la tâche asynchrone se termine sur le pool de threads en arrière-plan. Dans les mêmes contextes que oxphp_sleep(), il attend plutôt sur le thread worker

Ces fonctions ne suspendent pas la Fiber :

Fonction Comportement
oxphp_stream_flush() Envoie immédiatement un fragment au client et retourne. À utiliser avec oxphp_sleep() dans les boucles SSE
oxphp_finish_request() Envoie la réponse complète et poursuit l'exécution PHP. Ne rend pas la main
Note

Les fonctions natives sleep() et usleep() de PHP bloquent par défaut l'intégralité du thread worker. Utilisez oxphp_sleep() / oxphp_usleep(), ou définissez RUNTIME_HOOKS=sleep pour que les fonctions natives suspendent elles aussi la Fiber. C'est utile quand du code tiers appelle sleep() directement. Voir Configuration.

Là où rien ne se suspend

Les fonctions oxphp_* comme les fonctions natives interceptées ne se suspendent que là où un changement de Fiber est possible. Ailleurs, elles attendent plutôt sur le thread worker. Les fonctions natives interceptées confient l'appel aux sleep()/usleep() natifs : l'attente, et ce que renvoie sleep() lorsqu'un signal l'interrompt, sont donc ceux de la fonction native. Cela couvre un gestionnaire declare(ticks), la distribution des signaux de pcntl, la construction des entrées d'une requête, un FILTER_CALLBACK issu de filter_input_array() et les destructeurs exécutés après le démontage d'une requête ; la liste n'est pas close et figure en entier sous Hooks d'exécution.

Une Fiber que l'application démarre elle-même (avec new Fiber, ou via un ordonnanceur qu'elle pilote) est un cas à part : OxPHP ne peut pas reprendre un contexte qui ne lui appartient pas, donc un sommeil à l'intérieur de celle-ci occupe un thread worker. Dimensionnez cette part de la charge comme si le hook était désactivé.

Pour confirmer que le serveur a pris en compte le réglage, lisez runtime_hooks dans oxphp_server_info() ou dans /config du serveur interne. Une variable d'environnement non reconnue n'est une erreur nulle part.

Exemples PHP

Traitement concurrent de base

Les requêtes qui ne se suspendent pas s'exécutent à pleine vitesse, sans surcoût lié aux Fibers :

worker.php
<?php oxphp_worker(function () { $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); if ($path === '/health') { echo json_encode(['status' => 'ok']); return; // No suspension — runs at full speed } if ($path === '/slow') { oxphp_sleep(2.0); // Yields for 2 seconds — other requests run echo "Done after 2s delay"; return; } echo "Hello"; });

Appels d'API non bloquants

Combinez oxphp_async() et oxphp_async_await() pour effectuer des appels d'API externes sans bloquer le worker :

worker.php
<?php oxphp_worker(function () { // Dispatch two API calls to the async thread pool $p1 = oxphp_async(fn() => file_get_contents('https://api.example.com/users')); $p2 = oxphp_async(fn() => file_get_contents('https://api.example.com/orders')); // Await both — the fiber suspends, other requests run on this thread $users = oxphp_async_await($p1); $orders = oxphp_async_await($p2); header('Content-Type: application/json'); echo json_encode(['users' => json_decode($users), 'orders' => json_decode($orders)]); });

SSE avec sommeil coopératif

Entrelacez les Server-Sent Events avec d'autres requêtes à l'aide de oxphp_stream_flush() et oxphp_sleep() :

worker.php
<?php oxphp_worker(function () { header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); for ($i = 0; $i < 30; $i++) { echo "data: " . json_encode(['count' => $i, 'time' => time()]) . "\n\n"; oxphp_stream_flush(); // Send chunk now (does not suspend) oxphp_sleep(1.0); // Yield for 1 second (other requests run) } });

E/S bloquantes

Le multiplexage de Fibers est coopératif, pas préemptif. Une Fiber qui appelle une fonction bloquante gèle l'intégralité du thread worker : aucune autre Fiber de ce thread ne peut progresser.

Fonctions qui bloquent le worker

  • file_get_contents(), fopen(), fread()
  • curl_exec(), curl_multi_exec()
  • Les requêtes PDO, mysqli_query()
  • sleep(), usleep() de PHP (utilisez plutôt oxphp_sleep(), ou activez RUNTIME_HOOKS=sleep, dans les limites décrites sous Points de suspension)
  • La résolution DNS (gethostbyname())
  • Toute E/S réseau ou disque synchrone

Les lectures de sockets et stream_select() font exception une fois RUNTIME_HOOKS=streams activé : les deux suspendent la Fiber au lieu du thread, ce qui couvre les clients qui bloquent sur la lecture d'un flux PHP (mysqlnd, phpredis, les wrappers de flux HTTP) comme les boucles qui attendent sur plusieurs sockets à la fois. Les écritures, socket_select(), ext/curl, l'établissement de connexion et la résolution DNS bloquent toujours avec streams seul. La catégorie net suspend en outre l'établissement de connexion (sauf pour les connexions persistantes), les poignées de main TLS et les lectures, les lectures sur les sockets unix://, udp:// et udg://, les paires de sockets et les flux exportés depuis des sockets qui ne sont pas TCP, ainsi que les écritures qui attendent de la place, tandis que socket_select(), ext/curl et la résolution DNS bloquent toujours. La présence effective de ces catégories est indiquée par runtime_hooks dans oxphp_server_info() et dans /config. Voir Configuration.

Comment éviter le blocage

Enveloppez les opérations bloquantes dans oxphp_async() pour les exécuter sur le pool de threads asynchrone :

php
<?php // WRONG — blocks the entire worker thread $html = file_get_contents('https://example.com'); // CORRECT — runs on async pool, fiber yields $promise = oxphp_async(fn() => file_get_contents('https://example.com')); $html = oxphp_async_await($promise);

Pour les requêtes de base de données :

php
<?php $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); // WRONG — blocks the worker $users = $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); // CORRECT — query runs on async thread, fiber yields $promise = oxphp_async(function () { $db = new PDO('mysql:host=db;dbname=app', 'root', 'secret'); return $db->query('SELECT * FROM users WHERE active = 1')->fetchAll(); }); $users = oxphp_async_await($promise);
Note

Les connexions de base de données ne peuvent pas être passées à oxphp_async(), car les objets ne sont pas sérialisables entre les threads. Créez la connexion à l'intérieur de la closure asynchrone, ou effectuez la requête directement dans la Fiber si celle-ci est suffisamment rapide pour que le blocage soit acceptable.

Warning

oxphp_async() nécessite ASYNC_WORKERS > 0. Lorsque le pool asynchrone est désactivé (par défaut), l'appel à oxphp_async() lève OxPHP\Async\AsyncException.

Comment les Fibers sont recyclées

Les piles C des Fibers sont allouées une seule fois et réutilisées d'une requête à l'autre. Lorsqu'une Fiber termine le traitement d'une requête, elle n'est pas détruite ; elle se suspend plutôt et rend la main à l'ordonnanceur, puis elle est ajoutée à une liste libre. La requête suivante réutilise la pile C existante, ce qui évite une allocation mémoire coûteuse.

Les piles de la VM PHP (utilisées pour les cadres d'appel de fonction) sont conservées de la même manière : celle de chaque Fiber est allouée au premier démarrage de la Fiber et réutilisée par toutes les requêtes qu'elle sert. Une requête dont la chaîne d'appels la dépasse obtient des pages supplémentaires, rendues à la fin de ces appels.

Exemple Docker

compose.yaml
services: app: image: ghcr.io/oxphp/oxphp:0.13.0 ports: - "80:80" environment: - DOCUMENT_ROOT=/var/www/html/public - WORKER_MODE_ENABLED=true - ENTRY_FILE=worker.php - PHP_WORKERS=4 - ASYNC_WORKERS=8

Avec cette configuration, chacun des 4 threads worker peut gérer jusqu'à 256 Fibers concurrentes, et les E/S bloquantes sont déchargées vers 8 threads worker asynchrones.

Dépannage

Les requêtes deviennent lentes lorsqu'une requête effectue de lourdes E/S

Une Fiber appelle une fonction bloquante (requête de base de données, requête HTTP, lecture de fichier) sans oxphp_async(). Cela bloque l'intégralité du thread worker.

Correctif : Enveloppez les appels bloquants dans oxphp_async() :

php
<?php $promise = oxphp_async(fn() => file_get_contents($url)); $result = oxphp_async_await($promise);
"Async pool is disabled. Set ASYNC_WORKERS > 0 to enable."

Le pool asynchrone n'est pas configuré. Lorsque ASYNC_WORKERS=0 (par défaut), toutes les fonctions asynchrones lèvent OxPHP\Async\AsyncException.

Correctif : Définissez ASYNC_WORKERS sur une valeur positive :

bash
ASYNC_WORKERS=8
"Failed to dispatch async task" lors de l'utilisation de oxphp_async()

Le pool asynchrone fonctionne, mais il a atteint sa capacité maximale.

Correctif : Augmentez ASYNC_WORKERS ou ASYNC_QUEUE_CAPACITY :

bash
ASYNC_WORKERS=8 ASYNC_QUEUE_CAPACITY=512
oxphp_sleep() ne rend pas la main aux autres requêtes

Le multiplexage de Fibers ne fonctionne qu'en mode worker. En mode traditionnel, oxphp_sleep() se rabat sur un usleep() bloquant.

Correctif : Activez le mode worker en définissant WORKER_MODE_ENABLED=true.

Si le mode worker est déjà activé, l'appel se trouve à un endroit où un changement de Fiber est refusé ou dont OxPHP ne possède pas le contexte, comme énuméré sous Points de suspension. Une Fiber que l'application a démarrée elle-même (avec new Fiber, ou via un ordonnanceur qu'elle pilote au sein de la requête) est la manière la plus courante d'en arriver là, et aucun réglage n'y change rien.

Consommation mémoire élevée avec de nombreuses requêtes concurrentes

Chaque Fiber s'exécute sur sa propre pile C, dimensionnée par le paramètre ini fiber.stack_size de PHP (2 MiB sur les plateformes 64 bits lorsque le paramètre n'est pas défini), plus sa propre pile de la VM PHP. À la limite de 256 Fibers par thread worker, cela fait 512 MiB de pile C par thread, mais sous forme d'espace d'adressage réservé auprès du système d'exploitation et non de mémoire utilisée : une page de la pile d'une Fiber ne devient résidente qu'une fois que le code exécuté sur cette Fiber l'atteint. Un worker conserve chaque Fiber qu'il crée, piles comprises, jusqu'à sa sortie, et les réutilise pour les requêtes suivantes ; une Fiber dont une requête a un jour fait une récursion profonde garde donc ces pages tant que le worker vit.

Correctif : Si aucune requête n'a besoin d'une pile C plus profonde que la taille que vous choisissez, réduisez fiber.stack_size dans php.ini pour plafonner la part de sa pile qu'une Fiber peut occuper :

php.ini
fiber.stack_size = 512K

Cela réduit aussi l'espace d'adressage, à 128 MiB par thread worker à la limite de Fibers. La mémoire résidente ne baisse que là où une requête serait descendue plus profond que la nouvelle taille, et cette requête échoue désormais : l'appel qui irait plus profond lève Error: Maximum call stack size of … bytes … reached.

Limitations

  • Mode worker uniquement — le multiplexage de Fibers n'est pas disponible en mode traditionnel
  • 256 Fibers par worker — limite stricte, non configurable à l'exécution
  • Coopératif uniquement — le code gourmand en CPU (boucles serrées, calculs lourds) prive les autres Fibers de ressources. Il n'y a pas de préemption
  • Les E/S bloquantes bloquent le thread — tous les appels bloquants doivent être enveloppés dans oxphp_async() pour une véritable concurrence
  • Les fonctions natives sleep()/usleep() de PHP ne prennent pas en charge les Fibers — utilisez oxphp_sleep()/oxphp_usleep(), ou RUNTIME_HOOKS=sleep ; aucun des deux ne se suspend là où un changement de Fiber est refusé ou lorsqu'un ordonnanceur utilisateur possède le contexte (voir Points de suspension)
  • oxphp_async_await_race() et oxphp_async_await_any() ne rendent pas la main — elles bloquent actuellement même à l'intérieur d'une Fiber. oxphp_async_await_all() suspend bel et bien la Fiber pendant l'attente, elle est donc compatible avec les Fibers ; pour race/any, utilisez des appels séquentiels à oxphp_async_await() si vous avez besoin que le thread reste coopératif

Prompts pour votre agent

Ces prompts supposent un agent connecté au serveur MCP de la documentation OxPHP — il lira lui-même les pages dont il a besoin.

prompt
Rends mes gestionnaires de requêtes sûrs pour l'ordonnanceur coopératif d'OxPHP. Lis la page `features/fiber-multiplexing` via le serveur MCP de la documentation OxPHP. Passe ce dépôt en revue à la recherche des appels qui bloquent un thread worker au lieu de suspendre la Fiber, et dis-moi lesquels déplacer sur le pool asynchrone, lesquels un hook d'exécution couvre déjà, et lesquels sont assez courts pour être laissés tels quels.

Voir aussi

  • Mode worker — workers PHP persistants et l'API oxphp_worker()
  • Promesses asynchrones — pool de threads en arrière-plan pour décharger les E/S bloquantes
  • SSE — streaming en temps réel combiné à un sommeil coopératif basé sur les Fibers
  • Fonctions PHP — oxphp_sleep(), oxphp_usleep() et d'autres fonctions compatibles avec les Fibers
  • Référence de configuration — WORKER_MODE_ENABLED, ENTRY_FILE, PHP_WORKERS, ASYNC_WORKERS
Une erreur ? Signalez-la →